<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>꾸준함의 가치</title>
    <link>https://min-soon.tistory.com/</link>
    <description>비전공자의 백엔드 개발자 공부 기록 일지입니다.</description>
    <language>ko</language>
    <pubDate>Sun, 31 May 2026 20:00:40 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>mins0on</managingEditor>
    <image>
      <title>꾸준함의 가치</title>
      <url>https://tistory1.daumcdn.net/tistory/8498684/attach/36aba44cb1694b188728087301409ddc</url>
      <link>https://min-soon.tistory.com</link>
    </image>
    <item>
      <title>[Trouble Shooting] RefreshTokenService가 필요한 이유</title>
      <link>https://min-soon.tistory.com/69</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 상황&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;오늘은 JWT 로그인 유지 흐름을 확장하면서 RefreshTokenService를 만들었다.&lt;br /&gt;처음에는 Refresh Token을 생성하는 것까지만 생각했는데, 막상 구현하려고 보니 한 가지가 더 필요했다.&lt;br /&gt;&lt;br /&gt;Refresh Token을 발급만 하면 끝나는 것이 아니라, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;서버가 이 토큰을 저장하고 조회하고 삭제&lt;/b&gt;&lt;/span&gt;할 수 있어야 했다.&lt;br /&gt;그래서 RefreshTokenService를 따로 만들었다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;RefreshTokenService&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;RefreshTokenService는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Refresh Token을 Redis에 저장하고 관리&lt;/b&gt;&lt;/span&gt;하는 역할을 한다.&lt;br /&gt;로그인에 성공하면&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; 서버는 Refresh Token을 생성하고, 이 값을 Redis에 저장&lt;/b&gt;&lt;/span&gt;한다.&lt;br /&gt;&lt;br /&gt;예를 들어 다음과 같은 형태이다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1779978146329&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;key: refresh:{loginId}
value: refreshToken
TTL: 14일&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;TTL은 Redis에 저장된 데이터의 유효기간이다.&lt;br /&gt;Refresh Token은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;만료 시간과 Redis 저장 시간도 맞춰야 하기 때문에, TTL을 함께 설정&lt;/b&gt;&lt;/span&gt;한다.&lt;br /&gt;&lt;br /&gt;이렇게 저장해두면 이후에 Access Token이 만료됐을 때, &lt;br /&gt;재발급 요청에서 Redis에 저장된 Refresh Token과 요청으로 들어온 Refresh Token을 비교할 수 있다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/h3&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Service
public class RefreshTokenService {

    private static final String REFRESH_TOKEN_PREFIX = &quot;refresh:&quot;;

    private final StringRedisTemplate stringRedisTemplate;

    public void save(String loginId, String refreshToken, long refreshTokenValidityMs) {

        String key = createKey(loginId);

        stringRedisTemplate.opsForValue().set(
                key,
                refreshToken,
                Duration.ofMillis(refreshTokenValidityMs)
        );

    }

    public String get(String loginId) {
        String key = createKey(loginId);
        return stringRedisTemplate.opsForValue().get(key);
    }

    public void delete(String loginId) {
        stringRedisTemplate.delete(createKey(loginId));
    }

    private String createKey(String loginId) {
        return REFRESH_TOKEN_PREFIX + loginId;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;StringRedisTemplate&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;위 코드의 핵심은 StringRedisTemplate이다.&lt;br /&gt;&lt;br /&gt;StringRedisTemplate은 Spring에서 Redis에 문자열 데이터를 저장하고 조회할 때 사용한다.&lt;br /&gt;Redis는 key-value 형태로 데이터를 저장하는데, Refresh Token도 문자열이기 때문에 StringRedisTemplate을 사용한다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;StringRedisTemplate.opsForValue().set()&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;opsForValue()는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Redis의 String 타입 데이터를 다루겠다&lt;/b&gt;&lt;/span&gt;는 의미이다.&lt;br /&gt;&lt;br /&gt;set()은 Redis에 값을 저장하는 메서드이고,&amp;nbsp;&lt;br /&gt;key에는 refresh:{loginId} 같은 저장 이름이&lt;br /&gt;refreshToken은 실제 저장할 값이,&lt;br /&gt;Duration.ofMillis(refreshTokenValidityMs)는 데이터가 Redis에서 유지될 시간이 저장된다&lt;br /&gt;&lt;br /&gt;즉, 이 코드는 Refresh Token을 Redis에 저장하면서 만료 시간까지 함께 설정하는 코드이다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이번 작업을 통해 RefreshTokenService가 Redis에 Refresh Token을 저장하고 관리하는 역할이라는 것을 이해했다.&lt;br /&gt;특히&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; StringRedisTemplate과 opsForValue().set()을 통해 key-value 형태로 값을 저장하고 TTL을 설정하는 흐름&lt;/b&gt;&lt;/span&gt;을 확인했다. Refresh Token 관리는 생성뿐만 아니라 저장, 조회, 삭제까지 함께 봐야 한다는 점을 배웠다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Trouble Shooting</category>
      <category>#RefreshToken #JwtProvider #StringRedisTemplate</category>
      <author>mins0on</author>
      <guid isPermaLink="true">https://min-soon.tistory.com/69</guid>
      <comments>https://min-soon.tistory.com/69#entry69comment</comments>
      <pubDate>Thu, 28 May 2026 23:49:04 +0900</pubDate>
    </item>
    <item>
      <title>[Trouble Shooting] Access Token과 Refresh Token은 왜 나눠서 보관할까?</title>
      <link>https://min-soon.tistory.com/68</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;%EB%AC%B8%EC%A0%9C%20%EC%83%81%ED%99%A9-1&quot; style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 상황&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이번 프로젝트에서는, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;로그인 유지 방식을 JWT 방식&lt;/b&gt;&lt;/span&gt;을 이용했다. JWT 방식으로 구현하던 와중 궁금한 점이 생겼다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Access&amp;nbsp;Token과&amp;nbsp;Refresh&amp;nbsp;Token을&amp;nbsp;왜&amp;nbsp;굳이&amp;nbsp;나눠서&amp;nbsp;관리&lt;/b&gt;&lt;/span&gt;하지?&lt;br /&gt;그냥&amp;nbsp;토큰&amp;nbsp;하나만&amp;nbsp;오래&amp;nbsp;쓰면&amp;nbsp;안&amp;nbsp;되나?&lt;br /&gt;&lt;br /&gt;처음에는&amp;nbsp;토큰을&amp;nbsp;두&amp;nbsp;개로&amp;nbsp;나누는&amp;nbsp;구조가&amp;nbsp;오히려&amp;nbsp;복잡해&amp;nbsp;보였다.&lt;br /&gt;하지만 각각의 역할을 나눠보니 이유가 조금씩 이해됐다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; Access&amp;nbsp;Token의&amp;nbsp;역할&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Access Token은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;실제 API 요청에 사용&lt;/b&gt;&lt;/span&gt;하는 토큰이다.&amp;nbsp;&lt;br /&gt;예를&amp;nbsp;들어&amp;nbsp;로그인한&amp;nbsp;사용자가&amp;nbsp;내&amp;nbsp;정보를&amp;nbsp;조회할&amp;nbsp;때는&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;요청&amp;nbsp;헤더에&amp;nbsp;Access&amp;nbsp;Token을&amp;nbsp;담아&lt;/b&gt;&lt;/span&gt;&amp;nbsp;보낸다.&lt;br /&gt;&lt;br /&gt;GET&amp;nbsp;/api/user/me&lt;br /&gt;Authorization:&amp;nbsp;Bearer&amp;nbsp;accessToken&lt;br /&gt;백엔드는 이&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; Access Token을 검증해서 사용자가 인증된 사용자인지 확인&lt;/b&gt;&lt;/span&gt;한다.&lt;br /&gt;&lt;br /&gt;즉 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Access Token은 API에 접근할 때 사용하는 출입증 같은 역할&lt;/b&gt;&lt;/span&gt;을 한다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;하지만 Access Token은 요청마다 자주 사용된다. &lt;br /&gt;프론트에서 꺼내서 Authorization 헤더에 붙여야 하므로, 상대적으로 노출 위험이 있다.&lt;br /&gt;&lt;br /&gt;그래서&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; Access Token은 보통 유효 시간을 짧게&lt;/b&gt;&lt;/span&gt; 가져간다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Access Token&lt;br /&gt;-&amp;nbsp;API&amp;nbsp;요청에&amp;nbsp;사용&lt;br /&gt;-&amp;nbsp;자주&amp;nbsp;사용됨&lt;br /&gt;-&amp;nbsp;탈취&amp;nbsp;위험이&amp;nbsp;있으므로&amp;nbsp;짧게&amp;nbsp;유지&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; Refresh Token의 역할&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Refresh Token은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;API 요청에 직접 사용하는 토큰이 아니다&lt;/b&gt;&lt;/span&gt;.&lt;br /&gt;Refresh&amp;nbsp;Token의&amp;nbsp;역할은&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&amp;nbsp;Access&amp;nbsp;Token이&amp;nbsp;만료됐을&amp;nbsp;때,&amp;nbsp;새&amp;nbsp;Access&amp;nbsp;Token을&amp;nbsp;발급&lt;/b&gt;&lt;/span&gt;받는&amp;nbsp;것이다.&lt;br /&gt;&lt;br /&gt;흐름은&amp;nbsp;다음과&amp;nbsp;같다.&lt;br /&gt;&lt;br /&gt;Access Token 만료&lt;br /&gt;-&amp;gt;&amp;nbsp;/api/auth/reissue&amp;nbsp;요청&lt;br /&gt;-&amp;gt;&amp;nbsp;Refresh&amp;nbsp;Token&amp;nbsp;검증&lt;br /&gt;-&amp;gt;&amp;nbsp;새&amp;nbsp;Access&amp;nbsp;Token&amp;nbsp;발급&lt;br /&gt;&lt;br /&gt;즉 Refresh Token은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;재발급용 토큰&lt;/b&gt;&lt;/span&gt;이다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Access&amp;nbsp;Token보다&amp;nbsp;오래&amp;nbsp;유지되지만,&amp;nbsp;그만큼&amp;nbsp;탈취되면&amp;nbsp;위험하다.&lt;br /&gt;그래서&amp;nbsp;Refresh&amp;nbsp;Token은&amp;nbsp;프론트&amp;nbsp;JavaScript가&amp;nbsp;쉽게&amp;nbsp;접근할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;곳에&amp;nbsp;두지&amp;nbsp;않는&amp;nbsp;것이&amp;nbsp;좋다.&lt;br /&gt;&lt;br /&gt;왜 하나의 토큰만 오래 쓰면 안 될까?&lt;br /&gt;만약 Access Token 하나만 사용하고, 유효 기간을 14일로 길게 잡는다고 생각해보자.&lt;br /&gt;&lt;br /&gt;그러면 구현은 단순해지지만, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;토큰이 탈취되면 공격자는 14일 동안 계속 API에 접근&lt;/b&gt;&lt;/span&gt;할 수 있다.&lt;br /&gt;서버가&amp;nbsp;별도로&amp;nbsp;토큰을&amp;nbsp;관리하지&amp;nbsp;않는다면&amp;nbsp;중간에&amp;nbsp;강제로&amp;nbsp;무효화하기도&amp;nbsp;어렵다.&lt;br /&gt;&lt;br /&gt;그래서 보통 역할을 나누는 것이 일반적이다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Token 역할 분리&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;다음과 같이 역할을 분리하면 Access Token이 노출되더라도 피해 시간을 줄일 수 있고,&lt;br /&gt;사용자는&amp;nbsp;Refresh&amp;nbsp;Token&amp;nbsp;덕분에&amp;nbsp;매번&amp;nbsp;다시&amp;nbsp;로그인하지&amp;nbsp;않아도&amp;nbsp;된다.&lt;br /&gt;&lt;br /&gt;Access Token - 짧게 사용&lt;br /&gt;Refresh Token - 더 안전한 위치에 보관&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;HttpOnly Cookie&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;HttpOnly Cookie는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;브라우저에 저장되는 쿠키 중에서 JavaScript로 접근할 수 없도록 막은 쿠키&lt;/b&gt;&lt;/span&gt;다.&lt;br /&gt;일반 쿠키는 JavaScript에서 document.cookie로 읽을 수 있다.&lt;br /&gt;&lt;br /&gt;하지만 HttpOnly 옵션이 붙은 쿠키는 위와 같이 읽을 수 없다.&lt;br /&gt;브라우저는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;쿠키를 보관하고, 해당 서버로 요청을 보낼 때 자동으로 함께 전송&lt;/b&gt;&lt;/span&gt;한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;HttpOnly Cookie에 저장하는 이유&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;JWT 구조에서 Refresh Token을 HttpOnly Cookie에 저장하는 이유는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;오래 살아있는 토큰이기 때문에 더 안전하게 보관&lt;/b&gt;&lt;/span&gt;해야 하기 때문이다.&lt;br /&gt;&lt;br /&gt;그래서 프론트 JavaScript가 직접 읽을 수 있는 브라우저 저장소인 localStorage나 sessionStorage보다 HttpOnly Cookie를 사용하는 방식이 많이 쓰인다.&lt;br /&gt;&lt;br /&gt;한 마디로, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;HttpOnly Cookie는 JavaScript로 읽을 수 없고, 브라우저가 요청 시 자동으로 서버에 보내는 쿠키&lt;/b&gt;&lt;/span&gt;이다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Redis&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;HttpOnly Cookie는 브라우저에 안전하게 보관하는 방법이라면, Redis는 서버가 Refresh Token을 관리하는 방법&lt;/b&gt;&lt;/span&gt;이다.&lt;br /&gt;HttpOnly Cookie는 클라이언트 쪽 저장 위치이고, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Redis는 서버 쪽 저장&lt;/b&gt;&lt;/span&gt; 위치이다.&lt;br /&gt;&lt;br /&gt;즉 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Refresh Token을 한 군데에 두는 게 아니라, 양쪽에서 다룬다&lt;/b&gt;&lt;/span&gt;고 생각하면 된다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;로그인 성공&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 백엔드가 Refresh Token 생성 &lt;br /&gt;2. 브라우저에는 HttpOnly Cookie로 저장&lt;br /&gt;3. 서버는 같은 Refresh Token을 Redis에도 저장&lt;/blockquote&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;재발급 요청&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 브라우저가 Cookie에 담긴 Refresh Token을 자동 전송 &lt;br /&gt;2. 백엔드가 그 Refresh Token을 꺼냄 &lt;br /&gt;3. Redis에 저장된 Refresh Token과 비교 &lt;br /&gt;4. 같으면 새 Access Token 발급&lt;/blockquote&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이처럼 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Redis가 필요한 이유는 서버가 Refresh Token을 기억&lt;/b&gt;&lt;/span&gt;해야 하기 때문이다.&lt;br /&gt;&lt;br /&gt;JWT 자체는 서명만 맞으면 유효하다고 판단할 수 있다.&lt;br /&gt;하지만 로그아웃, 강제 만료, 재발급 관리 등을 하려면 서버가 현재 허용중인 REfresh Token을 알고 있어야 하며, 이를 저장하는 곳으로 Redis를 사용하는 것이다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Access Token과 Refresh Token을 나누는 이유는 보안과 사용자의 편의성을 함께 가져가기 위해서이다.&lt;br /&gt;&lt;br /&gt;Access Token은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;API 요청마다 자주 사용되기 때문에 짧게 유지&lt;/b&gt;&lt;/span&gt;한다. &lt;br /&gt;하지만 Refresh Token은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Access Token을 재발급하기 위해 오래 유지되지만 그만큼 더 안전하게 보관&lt;/b&gt;&lt;/span&gt;해야 한다.&lt;br /&gt;&lt;br /&gt;그래서 Refresh Token은 브라우저에서는 HttpOnly Cookie로 보관하고, 서버에서는 Redis에 저장해 유효성을 관리한다.&lt;br /&gt;&lt;br /&gt;이번 정리를 통해&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; JWT 로그인 유지 방식은 단순히 토큰을 발급하는 것이 아니라,&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;토큰의 역할과 저장 위치를 분리해 보안과 사용자 편의성을 함께 고려하는 구조&lt;/span&gt;&lt;/b&gt;라는 것을 알 수 있었다.&lt;/blockquote&gt;</description>
      <category>Trouble Shooting</category>
      <category>#API #AccessToken #RefreshToken #HttpOnly #Redis</category>
      <author>mins0on</author>
      <guid isPermaLink="true">https://min-soon.tistory.com/68</guid>
      <comments>https://min-soon.tistory.com/68#entry68comment</comments>
      <pubDate>Wed, 27 May 2026 22:42:49 +0900</pubDate>
    </item>
    <item>
      <title>[Trouble Shooting] JWT 필터에서 토큰이 없는데 바로 401을 던지지 않는 이유</title>
      <link>https://min-soon.tistory.com/67</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 상황&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;JWT 인증 필터를 만들면서 이런 코드를 작성했고, 이 부분이 이상하게 느껴졌다.&lt;br /&gt;&lt;br /&gt;토큰이 없다는 건 인증되지 않은 요청이라는 뜻인데, 왜 바로 예외를 던지거나 401 Unauthorize 응답을 던지지 않고 다음 필터로 넘기는 걸까?&lt;br /&gt;&lt;br /&gt;JWT가 없으면 잘못된 요청이 아닐까, 바로 막아야 하는 게 아닐까?라는 생각을 했지만 Spring Security의 인증 흐름을 조금 더 보니 잘못된 생각이었음을 깨달았다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1779804859355&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String authorization = request.getHeader(&quot;Authorization&quot;);

if (authorization == null || !authorization.startsWith(&quot;Bearer &quot;)) {
    filterChain.doFilter(request, response);
    return;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;JWT 필터는 모든 요청을 지난다&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;JWT 필터는 보통 Spring Security 필터 체인에 등록된다.&lt;/blockquote&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이렇게 등록하면 JWT 필터는 로그인 요청, 회원가입 요청 등 여러 요청을 지나가게 된다.&lt;br /&gt;즉, JWT 필터 입장에서는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;지금 들어온 요청이 반드시 인증이 필요한 요청인지&lt;/b&gt;&lt;/span&gt; 알기 어렵다.&lt;br /&gt;&lt;br /&gt;예를 들어 다음 요청들은 토큰이 없어도 정상이어야 한다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1779805108980&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST /api/auth/login
POST /api/auth/signup
GET /api/regions/sidos&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;로그인하려는 사용자, 회원가입 하려는 사용자 등은 토큰이 없는 것이 정상적인 흐름이다.&lt;br /&gt;그런데 JWT 필터에서 토큰이 없다는 이유로 바로 401을 던지면 로그인이나 회원가입 요청까지 전부 막혀버릴 수 있다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;JWT 필터의 역할&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;JWT 필터의 역할은 모든 요청을 무조건 막는 것이 아닌,&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Authorization 헤더에 Bearer 토큰이 있으면 검증하고, 유효하면 인증 정보를 SecurityContext에 저장&lt;/b&gt;&lt;/span&gt;하는 역할이다.&lt;/blockquote&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

    String authorization = request.getHeader(AUTHORIZATION_HEADER);

    if (!hasAuthorization(authorization)) {
        filterChain.doFilter(request, response);
        return;
    }

    String token = authorization.substring(BEARER_PREFIX.length());

    if (!jwtProvider.validateToken(token)) {
        filterChain.doFilter(request, response);
        return;
    }

    String loginId = jwtProvider.getLoginId(token);

    UserDetails userDetails = customUserDetailService.loadUserByUsername(loginId);

    UsernamePasswordAuthenticationToken authentication =
            new UsernamePasswordAuthenticationToken(
                    userDetails,
                    null,
                    userDetails.getAuthorities()
            );

    SecurityContextHolder.getContext().setAuthentication(authentication);

    filterChain.doFilter(request, response);
}

private boolean hasAuthorization(String authorization) {
    return authorization != null &amp;amp;&amp;amp; authorization.startsWith(BEARER_PREFIX);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;위의 코드와 같이 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;토큰이 있으면 인증을 시도&lt;/b&gt;&lt;/span&gt;하고, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;토큰이 없으면 인증 정보를 만들 수 없기 때문에 다음 필터로&lt;/b&gt;&lt;/span&gt; 넘긴다.&lt;br /&gt;여기서 중요한 점은, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;다음 필터로 넘긴다고 해서 무조건 요청이 성공하는 것은 아니라는 점&lt;/b&gt;&lt;/span&gt;이다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1779805351889&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;filterChain.doFilter(request, response);&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;최종 판단은 SecurityConfig&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;토큰이 없는 요청을 허용할지 막을지는 JWT 필터가 아니라, SecurityConfig의 권한 설정이 판단한다.&lt;br /&gt;예를 들어 다음과 같은 설정이 있다고 가정하겠다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1779805459091&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.authorizeHttpRequests(auth -&amp;gt; auth
        .requestMatchers(&quot;/api/auth/**&quot;, &quot;/api/regions/**&quot;).permitAll()
        .anyRequest().authenticated()
)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;위의 설정은 /api/auth/**, /api/regions/**의 요청 외에는 인증이 필요하다는 요청이다.&lt;br /&gt;만약 토큰 없이 /api/user/me 같은 보호 API로 요청하면, JWT 필터는 그냥 넘기지만 Security 설정에서 authenticated() 조건을 만족하지 못한다.&lt;br /&gt;&lt;br /&gt;그 결과 Spring Security가 인증되지 않은 요청으로 판단하고, 401 또는 403 응답을 내린다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;처음에는 토큰이 없으면 JWT 필터에서 바로 막아야 한다고 생각했다.&lt;br /&gt;&lt;br /&gt;하지만 JWT 필터는 모든 요청을 지나가기 때문에, 토큰이 없다는 이유만으로 바로 막으면 로그인, 회원가입, 공개 API까지 막힐 수 있다.&lt;br /&gt;&lt;br /&gt;그래서 J&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;WT 필터는 직접 최종 판단을 하기보다, 토큰이 있으면 인증 정보를 만들고 없으면 다음 필터로&lt;/b&gt; &lt;/span&gt;넘긴다.&lt;br /&gt;그리고 최종적으로 해당 요청이 인증이 필요한지는 SecurityConfig의 설정이 판단한다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;흐름&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;요청 들어옴 &lt;br /&gt;-&amp;gt; JWT 필터 실행 &lt;br /&gt;-&amp;gt; Bearer 토큰이 있으면 검증 &lt;br /&gt;-&amp;gt; 유효하면 SecurityContext에 Authentication 저장&lt;br /&gt;-&amp;gt; 다음 필터로 이동&lt;br /&gt;-&amp;gt; SecurityConfig의 권한 설정으로 최종 허용/거부 판단&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이번 문제를 통해 JWT 필터의 역할을 더 명확히 이해할 수 있었다.&lt;br /&gt;JWT 필터는 모든 요청을 막는 것이 아닌, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;요청 토큰이 있을 때 Spring Security가 이해할 수 있는 인증 정보로 바꿔주는 역할&lt;/b&gt;&lt;/span&gt;에 가깝다.&lt;/blockquote&gt;</description>
      <category>Trouble Shooting</category>
      <category>TrobuleShooting #개발일지 #작업일지 #SpringSecurity #JWT #JWTFilter</category>
      <author>mins0on</author>
      <guid isPermaLink="true">https://min-soon.tistory.com/67</guid>
      <comments>https://min-soon.tistory.com/67#entry67comment</comments>
      <pubDate>Tue, 26 May 2026 23:30:36 +0900</pubDate>
    </item>
    <item>
      <title>[TownTalk] -  기술 스택 고민과 선택</title>
      <link>https://min-soon.tistory.com/65</link>
      <description>&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;프로젝트 기술 스택 선정&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이번 프로젝트는 지역 기반 커뮤니티 서비스이며, 초기에는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;REST API 기반 백엔드 서버를 구축&lt;/b&gt;&lt;/span&gt;하고 이후 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;모바일 앱 연동까지 고려&lt;/b&gt;&lt;/span&gt;하고 있다. 그래서 단순히 익숙한 기술을 고르기보다는, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;확장성, 유지보수성, 앱 연동 가능성, 배포 편의성을 기준으로 기술 스택을 선택&lt;/b&gt;&lt;/span&gt;했다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;1254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PJuTv/dJMcahdt53g/nTu63fch40aHSQCex48PbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PJuTv/dJMcahdt53g/nTu63fch40aHSQCex48PbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PJuTv/dJMcahdt53g/nTu63fch40aHSQCex48PbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPJuTv%2FdJMcahdt53g%2FnTu63fch40aHSQCex48PbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;430&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;1254&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Java 21&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이전 프로젝트에서는 Java 17을 사용했지만, 이번 프로젝트에서는 Java 21을 선택했다.&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; Java 21도 Java 17과 마찬가지로 LTS 버전이기 때문에 장기적으로 안정적인 유지보수&lt;/b&gt;&lt;/span&gt;가 가능하다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;vs Java 17&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Java 17과 비교했을 때 Java 21의 가장 큰 장점은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;최신 기능들이 더 안정적으로 반영&lt;/b&gt;&lt;/span&gt;되었다는 점이다. 대표적으로 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Virtual Thread가 정식 기능으로 포함&lt;/b&gt;&lt;/span&gt;되어, 많은 요청을 처리하는 서버 애플리케이션에서 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;더 가볍고 효율적인 동시성 처리&lt;/b&gt;&lt;/span&gt;가 가능해졌다. 또한&amp;nbsp;switch&amp;nbsp;문 개선, Record Pattern, Sequenced Collection 같은 기능들이 추가되어 코드를 더 간결하고 읽기 쉽게 작성할 수 있다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;즉, Java 17이 안정적인 선택지였다면 Java 21은 그 안정성을 유지하면서도 성능과 개발 편의성을 더 높인 버전이라고 볼 수 있다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;LTS 버전&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;LTS는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Long Term Support이라는 뜻으로, 장기간 지원되는 버전이라는 뜻&lt;/b&gt;&lt;/span&gt;이다.&amp;nbsp;&lt;br /&gt;Java는 계속 새 버전이 나오지만 ,모든 버전을 오래 지원하지 않기에, 보통 오래 안정적으로 사용할 수 있는 LTS 버전을 많이 사용한다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Virtual Thread&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Virtaul Thread는 Java 21에서 정식으로 들엉노 기능이다.&lt;br /&gt;기존에는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;요청 하나를 처리할 때 운영체제의 무거운 스레드를 사용&lt;/b&gt;&lt;/span&gt;하는 경우가 많았는데, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Virtaul Thread는 더 가벼운 스레드를 사용해서 많은 요청을 효율적으로 처리&lt;/b&gt;&lt;/span&gt;할 수 있다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Record Pattern&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Java 21에서는 record 관련 기능도 계속 개선됐다. 프로젝트에서는 요청/응답 DTO를 작성할 때 record를 활용해 불필요한 코드를 줄이고, 데이터 전달 객체를 더 간결하게 표현할 수 있다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Sequenced Collection&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Sequenced Collection은 Java 21에서 추가된 컬렉션 관련 기능이다. 쉽게 말해 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;순서가 있는 컬렉션을 더 일관되게&lt;/b&gt;&lt;/span&gt; 다룰 수 있게 해주는 기능이다.&lt;br /&gt;&lt;br /&gt;예를 들어 List, LinkedHashSet처럼 순서가 있는 자료구조에서 첫 번째 값, 마지막 값의 동작을 더 통일된 방식으로 사용이 가능하다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1779699324883&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;list.getFirst();
list.getLast();
list.reversed();&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring Boot 4.0.6&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이전에는 Spring Boot 3.5 버전을 사용했지만, 이번 프로젝트에서는 Spring Boot 4.0.6을 선택했다. Spring Boot 3.5도 충분히 안정적인 버전이지만, Spring Boot 4는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;더 최신 Spring 생태계를 기반&lt;/b&gt;&lt;/span&gt;으로 한다는 장점이 있다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Spring Boot 4는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Spring Framework 7 기반&lt;/b&gt;&lt;/span&gt;으로 동작하며, 최신 Java 환경과 더 잘 맞도록 개선되었다. 또한 기존에 사용하던 인증, 검증, 데이터베이스 연동, REST API 개발 방식은 그대로 유지하면서도 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;내부 구조와 의존성들이 최신 버전&lt;/b&gt;&lt;/span&gt;으로 정리되어 있다.&lt;br /&gt;&lt;br /&gt;즉, Spring Boot 3.5가 안정적인 실무형 버전이라면, Spring Boot 4.0.6은 앞으로의 확장성과 최신 기술 호환성을 고려한 선택이라고 볼 수 있다. 모바일 앱 연동, JWT 인증, API 확장 등을 계속 추가할 예정이기 때문에 처음부터 최신 버전으로 개발 환경을 구성했다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Gradle - Groovy&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;빌드 도구는 Gradle을 선택했다. Gradle은 Maven보다 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;설정이 유연하고 빌드 속도 측면에서도 장&lt;/b&gt;&lt;/span&gt;점이 있다. Spring Boot 프로젝트에서 기본적으로 많이 사용되며, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;의존성 관리와 테스트, 빌드 자동화에 적합&lt;/b&gt;&lt;/span&gt;하다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Supabase PostgreSQL&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;데이터베이스는 MySQL, MongoDB 등 다른 선택지도 있었지만, Supabase PostgreSQL을 사용했다. 이번 프로젝트에는 실무에서 많이 쓰는 PostgreSQL에 더 익숙해지고 싶었다.&lt;br /&gt;&lt;br /&gt;PostgreSQL은 관계형 데이터베이스이기 때문에 사용자, 게시글, 댓글, 좋아요, 지역 정보처럼 서로 관계가 명확한 데이터를 다루기에 좋다. 또한 데이터 정합성이 중요하고, 복잡한 조회가 필요한 커뮤니티 서비스와 잘 맞는다.&lt;/blockquote&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;vs MySQL&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;PostgreSQL, MySQL 모두 관계형 데이터베이스이다. 사용자, 게시글, 댓글 등 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;서로 관계가 있는 데이터를 저장 할 때 사&lt;/b&gt;&lt;/span&gt;용한다. 특히 MySQL은 사용하기 쉽고, 자료가 많아 일반적인 웹 서비스에 많이 사용되어 입문하기 편하다는 장점이 있다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;PostgreSQL은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;데이터 정합성, 복잡한 조회, 확장 기능에 강한&lt;/b&gt;&lt;/span&gt; 데이터베이스이다.&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;커뮤니티 서비스에는 1:N의 관계가 많다&lt;/b&gt;&lt;/span&gt;. 예를 들어 사용자 1명이 게시글 여러개를 작성 할 수 있고, 게시글 1개에 댓글이 여러개 작성될 수 있다.&lt;br /&gt;&lt;br /&gt;이러한 관계가 많아질수록 데이터가 정확하게 저장되고 조회되는 것이 중요한데, PostgreSQL은 이런 관계형 데이터와 복잡한 조회에 강점이 있다.&lt;/blockquote&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;PostgreSQL 장점&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;복잡한 조회에 강함&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;PostgreSQL은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;SQL 표준 기능과 고급 쿼리 기능을 폭 넓게 지원&lt;/b&gt;&lt;/span&gt;한다. 예를 들면 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;여러 테이블을 JOIN해서 조회 한다던지, 게시글 수 같은 집계, 조건별 정렬과 페이징 등&lt;/b&gt;&lt;/span&gt;이 있다. 커뮤니티 서비스에서는 단순히 게시글만 가져오는게 아니라 이러한 조회가 필요할 것이다.&amp;nbsp;&lt;/blockquote&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;JSON 데이터에 유연&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;MySQL도 JSON을 다룰 수 있지만, PostgreSQL이 JSON을 더 강력하고 유연하게 다루는 편이다.&lt;br /&gt;&lt;br /&gt;PostgreSQL은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;jsonb 타입을 제공하고 내부적으로 인덱스를 사용해 JOSN 내부 값 검색을 효율&lt;/b&gt;&lt;/span&gt;적으로 할 수 있다. 공식 문서에서도 jsonb 데이터의 키나 값을 효율적으로 검색하기 위해 GIN 인덱스를 사용할 수 있다고 설명한다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;MySQL도 JSON 타입은 있지만, JOSN 컬럼 자체를 바로 일반 컬럼처럼 인덱싱 하기보다는 특정 값을 직접 활용하는 방식을 많이 사용한다. 그래서 JSON을 많이 활용하는 구조에서는 PostgreSQL이 더 자연스럽다.&lt;/blockquote&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Supabase&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Supabase를 선택한 이유는 PostgreSQL을 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;클라우드 환경에서 쉽게 사용&lt;/b&gt;&lt;/span&gt;할 수 있기 때문이다. 직접 DB 서버를 설치하고 관리하지 않아도 되고, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;초기 개발 단계에서 빠르게 데이터베이스를 구성&lt;/b&gt;&lt;/span&gt;할 수 있다. 나중에 모바일 앱이나 웹 클라이언트와 연동할 때도 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;관리 화면, 인증 기능, API 연계 가능성 등에서 장점&lt;/b&gt;&lt;/span&gt;이 있다.&lt;/blockquote&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;실제 데이터베이스 = PostgreSQL&amp;nbsp;&lt;br /&gt;PostgreSQL을 관리해주는 클라우드 서비스 = Supabase&lt;br /&gt;Java 객체와 DB를 연결해주는 ORM = JPA&amp;nbsp;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&amp;nbsp;Spring Data JPA&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;ORM으로는 Spring Data JPA를 사용했다. SQL을 직접 작성하는 방식도 가능하지만, JPA를 사용하면 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;객체 중심으로 데이터를 다룰 수 있어 도메인 구조를 설계&lt;/b&gt;&lt;/span&gt;하기 좋다.&lt;br /&gt;&lt;br /&gt;예를 들어&amp;nbsp;User,&amp;nbsp;Post,&amp;nbsp;Comment&amp;nbsp;같은 엔티티를 만들고 이들 간의 관계를 코드로 표현할 수 있다. 반복적인 CRUD 코드도 줄일 수 있어 개발 생산성이 높아진다. 다만 복잡한 쿼리가 필요한 경우에는 JPQL이나 QueryDSL 같은 방식을 함께 고려할 수 있다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring Security, JWT&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;보안은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Spring Security를 기반&lt;/b&gt;&lt;/span&gt;으로 구성할 예정이다. 커뮤니티 서비스에서는 로그인, 회원별 권한, 게시글 작성 권한, 토큰 검증 같은 기능이 필수적이다.&lt;br /&gt;&lt;br /&gt;특히 모바일 앱까지 고려하면 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;세션 기반 인증보다는 JWT 방식&lt;/b&gt;&lt;/span&gt;이 더 적합하다. &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;JWT는 서버가 로그인 상태를 직접 저장하지 않고 토큰을 통해 사용자를 인증&lt;/b&gt;&lt;/span&gt;할 수 있기 때문에, 웹과 모바일 앱 모두에서 사용하기 좋다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring Validation&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;사용자 입력값 검증을 위해 Spring Validation을 사용했다. 회원가입, 로그인, 게시글 작성, 댓글 작성 같은 API에서는 잘못된 값이 들어오는 경우를 반드시 처리해야 한다.&lt;br /&gt;&lt;br /&gt;예를 들어 이메일 형식, 비밀번호 길이, 게시글 제목의 빈 값 여부 등을 DTO 단계에서 검증하면 컨트롤러와 서비스 코드가 더 깔끔해진다. 또한 API 응답의 일관성을 유지하기에도 좋다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;u&gt;&lt;i&gt;보일러 플레이트 코드란 기능 자체는 단순한데, Java 문법상 계속 반복해서 작성해야하는 코드를 의미한다.&lt;/i&gt;&lt;/u&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Lombok, Spring Boot DevTools&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Lombok은 반복적인 getter, setter, constructor 코드를 줄이기 위해 사용했다. 엔티티나 DTO가 많아질수록 보일러플레이트 코드가 늘어나는데, Lombok을 사용하면 코드가 훨씬 간결해진다.&lt;br /&gt;&lt;br /&gt;Spring Boot DevTools는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;개발 중 서버 재시작과 변경 사항 반영을 편하게 하기 위해 사용&lt;/b&gt;&lt;/span&gt;했다. 개발 생산성을 높이는 보조 도구이다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;REST API&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;nbsp;REST API는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;웹 프론트엔드, 모바일 앱, 외부 서비스와 연동&lt;/b&gt;&lt;/span&gt;하기 쉽기에, API 스타일은 REST API로 설계했다.&lt;br /&gt;&lt;br /&gt;현재는 백엔드 서버 중심으로 개발하더라도, 나중에 React 웹, Android/iOS 앱, 또는 Flutter/React Native 앱이 같은 API를 사용할 수 있다. 즉, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;클라이언트가 바뀌어도 백엔드 API는 재사용&lt;/b&gt;&lt;/span&gt;할 수 있다는 장점이 있다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Vite&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Vite는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;가볍고 빠르게 React 프론트엔드를 만들기 좋은 도구&lt;/b&gt;&lt;/span&gt;이다. Next.js는 라우팅, SEO, 백엔드 기능까지 포함한 React 프레임워크로 많은 기능을 갖고 있다.&lt;br /&gt;&lt;br /&gt;하지만 이번 프로젝트는 Spring Boot 백엔드 서버가 따로 있고, 프론트엔드는 그 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;API를 호출하는 역할이 중심&lt;/b&gt;&lt;/span&gt;이다.&lt;br /&gt;그래서 처음부터 Next.js처럼 많은 기능을 가진 프레임워크보다, React 화면 개발에 집중할 수 있고 설정이 비교적 단순한 Vite가 더 적합하다고 판단했다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이번 기술 스택은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;빠른 개발과 장기적인 확장을 모두 고려&lt;/b&gt;&lt;/span&gt;해서 선택했다. Java와 Spring Boot는 안정적인 백엔드 서버를 만들기 좋고, PostgreSQL은 관계형 데이터가 많은 커뮤니티 서비스에 적합하다. Supabase는 초기 데이터베이스 운영 부담을 줄여주며, REST API 구조는 이후 웹과 모바일 앱 연동을 쉽게 만든다.&lt;br /&gt;&lt;br /&gt;결국 핵심은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;백엔드를 특정 화면이나 클라이언트에 종속되지 않게 만드는 것&lt;/b&gt;&lt;/span&gt;이다. 지금은 서버와 API를 중심으로 개발하고, 이후에는 Vite 기반 웹 프론트엔드나 모바일 앱을 같은 API에 연결하는 방식으로 확장할 수 있다.&lt;/blockquote&gt;</description>
      <category>Project</category>
      <category>프로젝트 #환경세팅 #초기세팅 #SpringBoot #Project</category>
      <author>mins0on</author>
      <guid isPermaLink="true">https://min-soon.tistory.com/65</guid>
      <comments>https://min-soon.tistory.com/65#entry65comment</comments>
      <pubDate>Tue, 26 May 2026 09:15:31 +0900</pubDate>
    </item>
    <item>
      <title>[Trouble Shooting]  Spring Security formLogin이 React JSON API와 맞지 않았던 이유</title>
      <link>https://min-soon.tistory.com/66</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;로그인에서 formLogin 대신 AuthenticationManager를 사용한 이유&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;TownTalk 프로젝트에서 로그인 기능을 구현하고 있던 와중 처음에는 Spring Security의 formLogin 설정을 사용하려고 했다. formLogin을 사용하면 Spring Security가 로그인 요청을 직접 처리해주기 때문이다.&lt;br /&gt;&lt;br /&gt;예를 들어 loginProcessignUrl을 지정하면 URL에 대한 컨트롤러를 따로 만들지 않아도 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Security Filter가 요청을 가로채서 인증&lt;/b&gt;&lt;/span&gt;을 수행한다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;처음에는 이 구조가 편해 보여서&amp;nbsp;formLogin을 사용하려고 했다.&lt;br /&gt;하지만 실제로 적용해보니 현재 프로젝트 구조와는 잘 맞지 않았다. 이 프로젝트는&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; React에서 JSON 형식으로 로그인 요청&lt;/b&gt;&lt;/span&gt;을 보내고, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;백엔드는 그 요청에 대해 JSON 응답을 내려주는 방식&lt;/b&gt;&lt;/span&gt;으로 구성되어 있기 때문이다.&lt;br /&gt;&lt;br /&gt;반면 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Spring Security의 기본&amp;nbsp;formLogin은 HTML form 기반 로그인 흐름에 더 적합&lt;/b&gt;&lt;/span&gt;하다.그래서 React + JSON API 구조에서는 요청 처리 방식과 응답 방식이 맞지 않아 오히려 흐름이 헷갈렸다.&lt;br /&gt;&lt;br /&gt;이번 글에서는 왜&amp;nbsp;formLogin&amp;nbsp;대신&amp;nbsp;AuthenticationManager를 직접 호출하는 방식으로 로그인 흐름을 정리했는지 적어보려고 한다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;헷갈렸던 부분&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Spring Security를 제대로 적용해보는 건 이번이 처음이였다. 그래서 처음 formLogin을 설정하면서 loginPage와 loginProcessignUrl이 헷갈렸었다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1779713046151&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.formLogin(form -&amp;gt; form
        .loginPage(&quot;/login&quot;)
        .loginProcessingUrl(&quot;/api/auth/loginProc&quot;)
        .usernameParameter(&quot;loginId&quot;)
        .passwordParameter(&quot;password&quot;)
)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;처음에는 loginPage(&quot;/login&quot;)을 설정하면 React의 /login 화면과 자연스럽게 연결되는 줄 알았지만, 이는 React 화면을 찾아주는 설정이 아니다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;인증이 필요한 요청이 들어왔을 때 Spring Security가 이동시킬 로그인 페이지 URL을 지정&lt;/b&gt;&lt;/span&gt;하는 설정이다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;또 loginProcessingUrl(&quot;/api/auth/loginProc&quot;)은 컨트롤러에 직접 매핑하는 URL이 아니다. 이 URL은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Spring Security Filter가 가로채서 로그인 인증을 처리하는 URL&lt;/b&gt;&lt;/span&gt;이다.&lt;br /&gt;&lt;br /&gt;즉, 기본 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;formLogin 흐름은 서버가 HTML form 로그인을 처리하는 구조&lt;/b&gt;&lt;/span&gt;에 더 가깝다.&amp;nbsp;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 발생 이유&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;현재 프로젝트는 React 프론트와 Spring Boot 백엔드가 분리되어 있다.&lt;br /&gt;React는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;fetch로 JSON 요청&lt;/b&gt;&lt;/span&gt;을 보내고, 백엔드는 /api/.. 형태로 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;JSON 응답&lt;/b&gt;&lt;/span&gt;을 내려주고 있다.&lt;br /&gt;&lt;br /&gt;하지만 Spring Security의 기본 formLogin은 HTML form 요청을 기준으로 동작같으며, 형태는 다음과 비슷하다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1779713293475&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST /login
Content-Type: application/x-www-form-urlencoded

username=testuser&amp;amp;password=password123!&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;프로젝트 - 로그인 요청&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1779713330149&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST /api/auth/login
Content-Type: application/json

{
  &quot;loginId&quot;: &quot;testuser&quot;,
  &quot;password&quot;: &quot;password123!&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;요청 방식 자체가 다르기에, formLogin을 그대로 사용하면 흐름이 계속 헷갈렸다.&lt;br /&gt;특히 로그인 실패 시에도 React 입장에서는 HTML redirect보다 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;JSON 에러 응답이 필요&lt;/b&gt;&lt;/span&gt;했다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결 방향&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;현재 구조에서는&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; 로그인 API를 직접 만들고, 그 안에서 Spring Security의 인증 기능을 사용하는 방식&lt;/b&gt;&lt;/span&gt;이 더 자연스러웠다.&lt;br /&gt;그래서 login 요청을 처리하는 코드를 작성하고,&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; 내부에서 AuthenticationManager&lt;/b&gt;&lt;/span&gt;를 호출했다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1779713507003&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;authenticationManager.authenticate(
        new UsernamePasswordAuthenticationToken(
                request.loginId(),
                request.password()
        )
);&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이 코드는 직접 비밀번호를 비교하는 코드가 아니라, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;AuthenticationManager에게 인증 요청&lt;/b&gt;&lt;/span&gt;을 하는 코드이다.&lt;br /&gt;실제 사용자 조회는 UserDetailsService가 하고, 비밀번호 검증은 PasswordEncoder가 담당한다.&lt;/blockquote&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;변경 후 흐름&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;React LoginPage &lt;br /&gt;&amp;nbsp; -&amp;gt; POST /api/auth/login &lt;br /&gt;&amp;nbsp; -&amp;gt; AuthController &lt;br /&gt;&amp;nbsp; -&amp;gt; AuthenticationManager &lt;br /&gt;&amp;nbsp; -&amp;gt; UserDetailsService&lt;br /&gt;&amp;nbsp; -&amp;gt; PasswordEncoder&lt;br /&gt;&amp;nbsp; -&amp;gt; 인증 성공 또는 실패&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;React는 JOSN으로 로그인 요청&lt;/b&gt;&lt;/span&gt;을 보내고, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;백엔드는 API 컨트롤러에서 요청을 받아 Spring Security 인증 로직에 위임&lt;/b&gt;&lt;/span&gt;한다.&lt;br /&gt;&lt;br /&gt;React는 화면과 요청을 담당하고, Spring Security는 인증 검증을 담당하게 되어 역할이 더 명확해졌다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;서버 렌더링 기반의 HTML form 로그인이라면 Spring Security의 formLogin이 자연스럽다.&lt;br /&gt;하지만 현재 프로젝트처럼&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; React와 Spring Boot API 서버가 분리된 구조에서는 formLign보다 JSON 로그인 API를 직접 만들고, AuthenticationManager에 인증을 위임&lt;/b&gt;&lt;/span&gt;하는 방식이 적합했다.&lt;br /&gt;&lt;br /&gt;이러한 문제를 통해, Spring Security의 formLogin은 단순히 로그인 기능을 켜는 설정이 아니라,&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;HTML form 기반 인증 흐름을 전제로 한 구조&lt;/b&gt;&lt;/span&gt;라는 점을 이해하게 됐다.&lt;br /&gt;&lt;br /&gt;그리고 이번 점을 계기로&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; Spring Security Filter Chain를 더 깊이있게 공부해야 한다고 느꼈고, 바로 내부 구조와 흐름에 대해서 공부&lt;/b&gt;&lt;/span&gt;할 생각이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Trouble Shooting</category>
      <author>mins0on</author>
      <guid isPermaLink="true">https://min-soon.tistory.com/66</guid>
      <comments>https://min-soon.tistory.com/66#entry66comment</comments>
      <pubDate>Mon, 25 May 2026 22:05:56 +0900</pubDate>
    </item>
    <item>
      <title>[Migration] Spring Boot 마이그레이션 1단계 - 테스트 코드 작성</title>
      <link>https://min-soon.tistory.com/64</link>
      <description>&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Spring MVC 프로젝트를 Spring Boot로 마이그레이션하면서 가장 먼저 고민한 것은 &quot;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;마이그레이션 후 기존 기능이 정상적으로 동작하는지 어떻게 검증&lt;/b&gt;&lt;/span&gt;할 것인가&quot;였다.&lt;br /&gt;&lt;br /&gt;기존 방식대로라면 마이그레이션 후 브라우저를 열고 하나하나 클릭하며 확인해야 했다. 로그인, 회원가입, 관리자 페이지, 마이페이지 등 모든 기능을 수동으로 검증하는 것은 시간이 오래 걸리고, 놓치는 부분이 생길 수밖에 없었다.&lt;br /&gt;&lt;br /&gt;특히 이번 마이그레이션은 단순히 프로젝트를 Spring Boot에서 실행되게 만드는 작업이 아니었다. 이후 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Spring Security 도입, JPA 전환, Redis 세션 적용처럼 내부 구조를 계속 바꿔야 하는 작업&lt;/b&gt;&lt;/span&gt; 이었다.&lt;br /&gt;&lt;br /&gt;겉으로 보이는 화면과 기능은 그대로 유지되어야 하지만, 내부 구현은 계속 달라질 예정이었다. 그래서 본격적인 마이그레이션 전에 테스트 코드를 먼저 작성하기로 했다.&lt;br /&gt;&lt;br /&gt;테스트 코드가 있으면 마이그레이션 후 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;테스트를 실행하는 것만으로&lt;/span&gt; 기존 기능의 정합성을 1차 검증&lt;/b&gt;할 수 있다. 나에게 테스트 코드는 새 기능을 검증하기 위한 도구라기보다, 기존 기능을 지키기 위한 안전장치에 가까웠다.&lt;/blockquote&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마이그레이션 실행 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 테스트 코드 작성&lt;br /&gt;2. Spring Security 도입&lt;br /&gt;3. JPA 전환&lt;br /&gt;4. Redis 세션 적용&lt;br /&gt;5. GitHub Actions CI/CD 구성&lt;b&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;테스트 코드 작성 방식 선택 - BDD(Behavior Driven Development)&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;처음에는 테스트코드를 JUnit5 기본 방식으로 작성했다. 그런데 테스트 코드를 작성하다 보니 준비 단계에서 when()을 쓰고, 실행 단계에서도 when을 사용하니 작성하면서도 헷갈리는 부분이 있었다.&lt;br /&gt;&lt;br /&gt;그래서 given/when/then 패턴과 맞지 않는다는 것을 느꼈고, 다른 방식을 찾아보았다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;테스트 코드를 작성하면서 단순히 테스트 개수를 늘리는 것보다, 나중에 다시 봤을 때 의도를 이해할 수 있는 테스트를 작성하는 것이 더 중요하다고 생각했다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1779591387099&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;✔️ Mockito 기본 방식
when(userDao.selectUser(&quot;testUser&quot;)).thenReturn(mockUser);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779591387101&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;✔️ BDD 방식
given(userDao.selectUser(&quot;testUser&quot;)).willReturn(mockUser);  ✔️ 준비
SessionUser result = userService.login(&quot;testUser&quot;, &quot;password&quot;);  ✔️ 실행
assertThat(result).isNotNull();  ✔️ 검증&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;BDD 방식은&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&amp;nbsp;given/when/then 패턴과 자연스럽게 매핑&lt;/b&gt;&lt;/span&gt;되어, 테스트&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;코드의 의도가 명확&lt;/b&gt;&lt;/span&gt;하게 드러났다.&lt;br /&gt;또한 then().should()로&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;메서드 호출 여부를 검증하는 것도 가독성이 좋아&lt;/b&gt;&lt;/span&gt;서 BDD 방식을 선택했다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;테스트 범위 정하기&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;프로젝트 전체 기능을 한 번에 테스트하기는 어려웠다. 이 프로젝트는 팀 프로젝트였고, 수강 신청, 강의, 게시판, 댓글 등 여러 기능이 있었다. 그중 내가 주로 담당한 영역은 회원과 관리자 기능이었다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;그래서 테스트 범위도 먼저 내 담당 영역으로 좁혀, 회원 영역에서는 다음 흐름을 중심으로 테스트했다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;처음부터 모든 테스트를 완벽하게 작성하려고 했지만 오히려 진도가 나가지 않았기에, &quot;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;내가 구조를 바꿀 때 가장 자주 깨질 수 있는 부분&lt;/b&gt;&lt;/span&gt;&quot;부터 테스트했다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Controller 테스트&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Controller 테스트는 사용자의 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;요청과 응답 흐름을 확인하기 위해 작성&lt;/b&gt;&lt;/span&gt;했으며,&amp;nbsp; 사용한 방식은&amp;nbsp;@WebMvcTest와&amp;nbsp;MockMvc다.&amp;nbsp; 처음에는 Security까지 같이 테스트하는 것이 더 좋은 것 아닌가 생각했다. 하지만 이 단계의 목적은 컨트롤러 요청/응답 흐름을 확인하는 것이었다.&lt;br /&gt;&lt;br /&gt;Security는 별도의 테스트에서 검증하는 것이 더 명확하다고 판단했다. Controller 테스트에 Security까지 섞이면,&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; 테스트가 실패했을 때 컨트롤러 문제인지 권한 설정 문제인지 구분하기 어렵&lt;/b&gt;&lt;/span&gt;기 때문이다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Service 테스트&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Service 테스트는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;실제 비즈니스 로직을 확인하기 위해 작성&lt;/b&gt;&lt;/span&gt;했으며, 사용한 방식은&amp;nbsp;MockitoExtension이다.&lt;br /&gt;Service 테스트에서는 DB, 메일, Supabase Storage 같은 외부 의존성을 mock으로 대체했다.&lt;br /&gt;&lt;br /&gt;예를 들어 비밀번호 찾기 기능은 사용자 정보가 일치하면 임시 비밀번호를 발급하고 메일을 보내야 한다. 하지만 테스트에서 실제 메일을 보낼 필요는 없다. 중요한 것은 &quot;메일 발송 로직이 호출되었는지&quot;와 &quot;예외 상황에서 올바르게 실패하는지&quot;다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;이렇게 외부 의존성을 mock 처리하니 테스트가 빨라졌고, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;서비스 로직의 조건 분기에 집중&lt;/b&gt;&lt;/span&gt;할 수 있었다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;테스트를 작성하면서 발견한 점&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;테스트를 작성하면서 단순히 &quot;기능이 맞는지&quot;만 확인한 것은 아니었다. 기존 코드의 책임이 어디에 있는지도 더 잘 보이기 시작했다. 가장 대표적인 부분이 로그인이다.&lt;br /&gt;&lt;br /&gt;기존 프로젝트에서는 컨트롤러가 로그인 요청을 받고, 사용자 정보를 조회하고, 세션에 저장하는 흐름을 가지고 있었다. 그리고 인터셉터가 로그인 여부와 관리자 권한을 확인했다.&lt;br /&gt;&lt;br /&gt;하지만 Spring Security를 도입하면 로그인 요청은 Security 필터가 처리한다. 컨트롤러가 직접 로그인 로직을 가질 필요가 줄어든다. 이 차이를 테스트를 보면서 더 명확하게 알 수 있었다.&lt;br /&gt;&lt;br /&gt;기존 Controller 테스트에는 로그인 성공/실패 흐름이 들어있었지만, Security 전환 이후에는 이 테스트의 책임이 바뀌어야 한다. 로그인 컨트롤러 테스트가 아니라 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Security 필터 체인,&amp;nbsp;UserDetailsService, 로그인 성공/실패 핸들러 테스트로 분리&lt;/b&gt;&lt;/span&gt;하는 것이 더 맞다.&lt;br /&gt;&lt;br /&gt;즉,&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; 테스트 코드를 작성하면서 오히려 앞으로 리팩토링해야 할 방향&lt;/b&gt;&lt;/span&gt;이 보였다.&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Spring Security 도입 전에 도움이 된 부분&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;테스트 코드 덕분에 Spring Security를 도입할 때 기준을 잡을 수 있었다. Security 적용 후에는 다음 흐름을 우선 확인했다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 비로그인 사용자가 보호 URL에 접근하면 로그인 화면으로 이동하는가&lt;br /&gt;2. 일반 사용자가 /admin/**에 접근하면 권한 없음 처리가 되는가&lt;br /&gt;3. PENDING 계정으로 로그인하면 상태에 맞는 에러 메시지가 출력되는가&lt;br /&gt;4. 로그인 성공 후 SessionUser가 세션에 저장되는가&lt;br /&gt;5. 로그아웃 후 인증 정보와 세션이 정리되는&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이 과정에서&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; 기존 인터셉터가 담당하던 로그인 여부 확인과 관리자 권한 확인은 Spring Security로 옮기는 것이 맞다&lt;/b&gt;&lt;/span&gt;고 판단했다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;반대로 게시판 접근 권한이나 수강 신청 기간 체크처럼 다른 도메인의 비즈니스 규칙은 무리하게 제거하지 않았다. 이 부분은 내가 담당한 범위를 벗어나기도 하고, 단순 인증/인가와는 성격이 다르기 때문이다.&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;부족한 점&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;현재 작성한 테스트는 마이그레이션을 위한 첫 번째 안전망으로, 아직 부족한 부분도 많다. Security 필터를 포함한 인증/인가 테스트,&amp;nbsp;CustomUserDetailsService&amp;nbsp;계정 상태별 로그인 실패 테스트 등..&lt;br /&gt;&lt;br /&gt;특히 지금은 Controller 테스트에서 Security 필터를 제외하고 있기 때문에, 실제 권한 정책은 별도의 Security 테스트로 반드시 보강해야 한다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이번 단계에서 테스트 코드는 단순히 정답을 확인하는 도구가 아니었다. 마이그레이션 과정에서 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;기존 기능을 지키기 위한 기준점이었고, 동시에 코드의 책임을 다시 생각하게 만드는 도구&lt;/b&gt;&lt;/span&gt;였다.&lt;br /&gt;&lt;br /&gt;Spring Boot 마이그레이션은 앞으로 Security, JPA, Redis, CI/CD까지 계속 이어질 예정이다. &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;내부 구조는 계속 바뀌겠지만, 사용자가 경험하는 기능은 유지&lt;/b&gt;&lt;/span&gt;되어야 한다.&lt;br /&gt;&lt;br /&gt;그래서 앞으로의 작업도 테스트를 기준으로 하나씩 확인하면서 진행하려고 한다.&lt;/blockquote&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1a1c1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;한 줄 회고&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;테스트 코드를 먼저 작성해두니 마이그레이션이 단순한 코드 이동이 아니라, 기존 기능을 유지하면서 구조를 개선하는 작업이라는 점이 더 분명해졌다.&lt;/blockquote&gt;</description>
      <category>Migration</category>
      <author>mins0on</author>
      <guid isPermaLink="true">https://min-soon.tistory.com/64</guid>
      <comments>https://min-soon.tistory.com/64#entry64comment</comments>
      <pubDate>Mon, 25 May 2026 09:15:15 +0900</pubDate>
    </item>
    <item>
      <title>[회고] 첫 팀 프로젝트를 마치며</title>
      <link>https://min-soon.tistory.com/63</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;첫 팀 프로젝트를 마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 팀 프로젝트를 시작하기 전에는 내가 맡은 역할에 맞춰 충분히 공부했다고 생각했고, 어느 정도 자신감도 있었다. &lt;b&gt;실무에 가까운 구조와 간결한 코드를 목표&lt;/b&gt;로 삼고 프로젝트를 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로 프로젝트를 진행해보니 생각처럼 쉽지 않았다. 처음 진행하는 팀 프로젝트였고, 기능 구현 자체가 막히는 순간도 많았다. 그럴 때마다 조급함이 앞섰고, 설계나 구조를 충분히 고민하기보다 일단 기능을 완성하는 데 집중하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 많은 로직이 컨트롤러에 몰리는 비효율적인 구조가 만들어졌다. 프로젝트를 완성한 뒤 다시 코드를 돌아보니, 내가 직접 작성한 코드임에도 흐름을 한눈에 파악하기 어려웠고 수정하기도 부담스러웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 프로젝트였기 때문에 팀원의 코드를 이해하고 활용해야 하는 상황도 많았다. 하지만 코드의 의도가 명확하게 드러나지 않거나, 충분한 커뮤니케이션 없이 작성된 코드는 파악하는 데 시간이 오래 걸렸다. 이 경험을 통해 &lt;b&gt;코드 가독성과 협업에서의 커뮤니케이션이 얼마나 중요한지 체감&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 일부 코드를 리팩토링하면서 서비스 레이어를 분리하고, 공통 응답 처리에는 제네릭 와일드카드를 적용해보았다. 로직의 책임이 분리되자 코드의 의도가 더 명확해졌고, 흐름도 훨씬 쉽게 읽혔다. 이 과정에서 &lt;b&gt;코드의 간결함은 단순히 짧게 작성하는 것이 아니라, 가독성과 유지보수성을 높이는 방향이어야 한다는 것&lt;/b&gt;을 배웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 시작하기 전에는 팀 프로젝트가 끝나면 바로 새로운 개인 프로젝트를 준비할 생각이었다. 하지만 프로젝트를 마친 뒤 생각이 바뀌었다. 무작정 여러 프로젝트를 만드는 것보다, &lt;b&gt;하나의 프로젝트를 깊이 있게 돌아보고 부족했던 부분을 개선하는 과정이 더 중요&lt;/b&gt;하다고 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 프로젝트를 다시 살펴보며 &lt;b&gt;Spring Boot 기반으로 마이그레이션&lt;/b&gt;하고, 기존에 아쉬웠던 구조를 하나씩 개선해보기로 했다. 테스트 코드를 작성해 기존 기능을 검증하고, Spring Security로 인증/인가 구조를 정리하며, 이후 JPA와 Redis 세션, CI/CD까지 단계적으로 적용해볼 계획이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 경험을 통해 &lt;b&gt;기능을 완성하는 것만큼 중요한 것은 유지보수 가능한 구조를 만드는 것이라는 점&lt;/b&gt;을 배웠다. 다음 프로젝트에서는 처음부터 관심사의 분리와 코드 가독성을 더 의식하며 개발하고 싶다.&lt;/p&gt;</description>
      <category>Migration</category>
      <author>mins0on</author>
      <guid isPermaLink="true">https://min-soon.tistory.com/63</guid>
      <comments>https://min-soon.tistory.com/63#entry63comment</comments>
      <pubDate>Sun, 24 May 2026 12:16:23 +0900</pubDate>
    </item>
    <item>
      <title>[개념 정리] 포트</title>
      <link>https://min-soon.tistory.com/62</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;포트(Port)&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;컴퓨터 한 대에서는 지금도 브라우저, 카카오톡, Spring Boot 서버 등 수 많은 프로그램이 동시에 실행되고 있다.&amp;nbsp;&lt;br /&gt;그러면 외부에서 데이터가 들어올 때 운영체제는 이걸 어느 프로그램한테 전달해야 하는지 어떻게 알까?&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;포트는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;컴퓨터 안에서 실행 중인 각 프로그램에게 붙여주는 고유한 번호&lt;/b&gt;&lt;/span&gt;이다.&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; 0 ~ 65535까지 존재&lt;/b&gt;&lt;/span&gt;하며, 외부에서 데이터가&amp;nbsp; 들어오면 운영체제가 포트 번호를 보고 '8080으로 왔으니 Spring Boot에게 전달'이라고 판단을 한다.&lt;br /&gt;&lt;br /&gt;ex) IP 주소는 '서울시 강남구 xx아파트' 같은 건물 주소이고, 포트는 '110동 201호' 같은 호실 번호이다.&lt;br /&gt;✔️ 즉, 주소(IP)만 있으면 건물까지는 찾아가지만, 호실(포트)이 없으면 어디로 가야하는지 모른다.&lt;/blockquote&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;IP주소와 포트의 관계&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;네트워크에서 특정 컴퓨터의 특정 프로그램에 접근 하려면 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;IP 주소와 포트 번호를 함께 작성&lt;/b&gt;&lt;/span&gt;해야 한다. &lt;br /&gt;표기법은 IP 주소:포트번호 이다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;241&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDUjY2/dJMcahYOb6o/jBbuzoohMsaQPv4fvk2Fi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDUjY2/dJMcahYOb6o/jBbuzoohMsaQPv4fvk2Fi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDUjY2/dJMcahYOb6o/jBbuzoohMsaQPv4fvk2Fi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDUjY2%2FdJMcahYOb6o%2FjBbuzoohMsaQPv4fvk2Fi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;814&quot; height=&quot;241&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;241&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;같은 컴퓨터에서도 프로그램마다 다른 포트를 사용하고 있기 때문에, 서로 충돌 없이 동시에 실행될 수 있다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;localhost&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;localhost는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&quot;내 컴퓨터 자신&quot;을 가리키는 이름&lt;/b&gt;&lt;/span&gt;이다. IP 주소와 완전히 동일하며, 개발할 때 서버를 로컬에서 띄우고 브라우저로 접속할때 사용하는 주소이다.&lt;br /&gt;&lt;br /&gt;로컬에서 개발할 때 localhost는 개발자 본인만 접근할 수 있다. 외부에서 접근하려면 내 컴퓨터의 실제 IP나 배포된 서버 주소가 필요하다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개념 정리</category>
      <category>포트 #PORT #IP</category>
      <author>mins0on</author>
      <guid isPermaLink="true">https://min-soon.tistory.com/62</guid>
      <comments>https://min-soon.tistory.com/62#entry62comment</comments>
      <pubDate>Sat, 23 May 2026 09:15:20 +0900</pubDate>
    </item>
    <item>
      <title>[개념 정리] 리버스 프록시</title>
      <link>https://min-soon.tistory.com/61</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;리버스 프록시(Reverse Proxy)&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;리버스 프록시란 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;클라이언트의 요청을 대신 받아서 내부 서버로 전달&lt;/b&gt;&lt;/span&gt;하고, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;응답을 다시 클라이언트에게 돌려주는&lt;/b&gt; &lt;b&gt;서버&lt;/b&gt;&lt;/span&gt;이다. 클라이언트는 실제 서버가 어디 있는지 알 수 없다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;✔️ 보안 : 실제 서버 포트(8080 등)를 외부에 숨겨, 직접 공격을 차단 한다.&lt;br /&gt;✔️ SSL 처리 : HTTPS 인증서를 여기서 한 번만 처리하여, 내부 서버는 그냥 HTTP로 통신한다.&lt;br /&gt;✔️ 로드밸런싱 : 서버가 여러 대일 때 요청을 골고루 분산하여, 트래픽이 폭발해도 죽지 않는다.&lt;/blockquote&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;요청 흐름&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;클라이언트 입장에선 프록시 주소 하나만 알면 되고, 뒤에서 어떤 서버가 몇 대 돌아가는지는 몰라도 된다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;501&quot; data-origin-height=&quot;82&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ChCX1/dJMcagr4dgE/riDiUEOYW16gbb6e9a90Yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ChCX1/dJMcagr4dgE/riDiUEOYW16gbb6e9a90Yk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ChCX1/dJMcagr4dgE/riDiUEOYW16gbb6e9a90Yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FChCX1%2FdJMcagr4dgE%2FriDiUEOYW16gbb6e9a90Yk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;501&quot; height=&quot;82&quot; data-origin-width=&quot;501&quot; data-origin-height=&quot;82&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;즉, 리버스 프록시 서버는 다음과 같은 상황에서 쓴다&lt;br /&gt;&lt;br /&gt;✔️ 서버를 외부에 직접 노출하기 싫을 때&lt;br /&gt;✔️ 도메인 하나로 여러 서비스를 운영할 때&lt;br /&gt;✔️ HTTPS 인증서 관리를 한 곳에서 처리하고 싶을 때&lt;br /&gt;✔️ 트래픽이 많아져서 서버를 여러 대로 늘릴 때&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  nginx, Apache 같은 리버스 프록시 소프트웨어는 Linux, macOS, Windows 모두 동작하며, OS 종속 개념이 아닌 아키텍처 패턴이다. 다만, 실 서버는 거의 Linux 위에서 운영되기 때문에 Linux 환경에서 주로 접하게 된다.&amp;nbsp;&lt;/blockquote&gt;</description>
      <category>개념 정리</category>
      <category>리버스 프록시 #프록시 #Proxy</category>
      <author>mins0on</author>
      <guid isPermaLink="true">https://min-soon.tistory.com/61</guid>
      <comments>https://min-soon.tistory.com/61#entry61comment</comments>
      <pubDate>Fri, 22 May 2026 14:10:14 +0900</pubDate>
    </item>
    <item>
      <title>[운영체제] Linux의 기초 개념 이해 - 파일 시스템, 입출력, 권한 모델</title>
      <link>https://min-soon.tistory.com/60</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;디렉토리 및 파일 관리&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;리눅스에서는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;설정, 로그, 네트워크 소켓, 하드웨어 장치까지 모든 것이 파일로 추상화&lt;/b&gt;&lt;/span&gt;된다. 파일 시스템 구조를 이해하지 못하면 도커 볼륨을 마운트할 때, nginx 설정을 변경할 때, 로그를 추적할 때 매번 길을 잃게 된다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Eveything is a File&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Everything is a File은 리눅스의 핵심 철학으로 일반 파일, 디렉터리, 심볼릭 링크, 장치 파일, 소켓, 파이프 모두 파일 인터페이스로 다룹니다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;윈도우에서는 마우스, 키보드 모니터 같은 하드웨어나 네트워크 연결을 '장치 관리자'나 '제어판' 같은 특수한 프로그램으로 관리한다. 하지만, 리눅스는 무식할 정도로 단순하게 모든 걸 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;최상위 폴더인 '루트(/)' 아래의 파일과 디렉토리 구조&lt;/b&gt;&lt;/span&gt;로 만든다.&lt;br /&gt;&lt;br /&gt;즉, 리눅스에서 무언가를 제어하거나 데이터를 조회하고 싶다면 전용 프로그램 필요 없이 그냥 그 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;파일이 있는 위치로 찾아가 조회 및 수정&lt;/b&gt;&lt;/span&gt;을 하면 끝난다는 철학입니다.&lt;/blockquote&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;도커 볼륨 마운트&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;도커 컨테이너는 원래 일회성이다. 컨테이너를 삭제하면 그 안에서 생성된 로그나 사용자가 업로드한 파일, DB 데이터가 전부 날아간다.&lt;br /&gt;&lt;br /&gt;이 문제를 해결하기 위해 내 실제&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; 컴퓨터(호스트)의 특정 폴더와 도커 컨테이너 내부의 특정 폴더를 실시간으로 연동&lt;/b&gt;&lt;/span&gt;해 두는 것을 마운트라고 한다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;볼륨은 내 컴퓨터의 구체적인 경로를 신경 쓰지 않고, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;도커한테 저장 공간을 하나 만들어달라고 요청&lt;/b&gt;&lt;/span&gt;하는 방식이다. 그러면 도커가 리눅스 도커 관리 영역에 비밀 폴더를 하나 만들고 컨테이너와 연결해준다.&lt;br /&gt;&lt;br /&gt;즉, 도커가 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;데이터 유실을 막기 위해 내 컴퓨터 안에 안전하게 마련한 비밀 데이터 저장소&lt;/b&gt;&lt;/span&gt;이다.&lt;/blockquote&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Nginx 설정&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Nginx는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;가볍고 빠른 웹 서버이자, 요청을 뒤로 넘겨주는 리버스 프록시(Reverse Proxxy)역할&lt;/b&gt;&lt;/span&gt;을 하는 프로그램이다.&lt;br /&gt;실무에서는 주로 Nginx 설정 파일을 포트 포워딩 및 라우팅 할 때, HTTPS 보안 적용 등을 할때 사용한다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;FHS(Filesystem Hierarchy Standard) - 표준 디렉터리 구조&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;리눅스는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;FHS에 따라 디렉터리의 역할이 정해져&lt;/b&gt;&lt;/span&gt;있다. 외울 필요는 없지만, 자주 쓰는 경로는 익혀 두는 것이 좋다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;✔ # 루트 &amp;mdash; 모든 것의 시작점 &lt;br /&gt;├── bin/ # 기본 실행 파일 (ls, cp, mv&amp;hellip;) &lt;br /&gt;├── etc/ # 시스템 전역 설정 파일 &lt;br /&gt;├── home/ # 사용자 홈 디렉터리 &lt;br /&gt;├── var/ # 로그, 캐시 등 가변 데이터 &lt;br /&gt;├── tmp/ # 임시 파일 (재부팅 시 삭제) &lt;br /&gt;├── proc/ # 커널&amp;middot;프로세스 정보 (가상 FS) &lt;br /&gt;├── dev/ # 장치 파일 (sda, tty&amp;hellip;) &lt;br /&gt;└── usr/ # 사용자 설치 프로그램&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;필수 명령어&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;# 탐색 / 이동 &lt;br /&gt;$ pwd&amp;nbsp; # 현재 위치 출력 &lt;br /&gt;$ ls -alh /etc # 숨김파일 포함 상세 목록 &lt;br /&gt;$ cd ~ # 홈 디렉터리로 이동 &lt;br /&gt;$ cd - # 이전 디렉터리로 돌아가기 # 생성 / 복사 / 이동 / 삭제&lt;br /&gt;$ mkdir -p project/src/utils # 중간 디렉터리까지 한번에 생성 &lt;br /&gt;$ cp -r src/ backup/ # 디렉터리 재귀 복사 &lt;br /&gt;$ mv old.txt new.txt # 이름 변경 or 이동 &lt;br /&gt;$ rm -rf temp/ # ⚠ 강제 재귀 삭제 (복구 불가) # 파일 내용 확인 &lt;br /&gt;$ cat config.txt # 전체 출력 $ less large.log # 페이지 단위로 탐색 (q로 종료) &lt;br /&gt;$ tail -f /var/log/syslog # 로그 실시간 추적 # 검색 &lt;br /&gt;$ find . -name &quot;*.log&quot; -mtime -1 # 1일 이내 수정된 .log 파일 &lt;br /&gt;$ grep -rn &quot;ERROR&quot; ./logs/ # 재귀적으로 ERROR 문자열 검색&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;표준 입출력 및 리다이렉션&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;리눅스 명령어들은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;각자 독립적이지만, 표준 입출력이라는 공통 언어로 연결&lt;/b&gt;&lt;/span&gt;된다. 이 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;구조 덕분에 간단한 명령어들을 조합해서 복잡한 데이터 처리가 가능&lt;/b&gt;&lt;/span&gt;하다. 파이프(|)와 리다이렉션(&amp;gt;)을 모르면 셀 스크립트 작성이나 로그 분석이 힘들어진다.&lt;/blockquote&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;stdin / stdout / stderr&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;리눅스에서 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;모든 프로세스는 커널이 부여하는 3가지 표준 스트림(기본 입출력 통로)를 가지며&lt;/b&gt;&lt;/span&gt;, 각각 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;디스크립터(정수) 번호로(fd) 매핑&lt;/b&gt;&lt;/span&gt;되어 관리됩니다.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fd 0 (stdin / 표준 입력) : 프로세스로 데이터가 들어오는 통로(기본 값 : 키보드)&lt;/li&gt;
&lt;li&gt;fd 1 (stdout / 표준 출력) : 프로세스의 정상 처리 결과가 나가는 통로(기본 값 : 터미널 화면)&lt;/li&gt;
&lt;li&gt;fd 2 (stderr / 표준 에러) : 프로세스의 에러 메시지가 나가는 통로(기본 값 : 터미널 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;stdout과 stderr를 구분하는 이유는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;정상 출력만 파이프로 다음 명령어에 넘기고, 에러는 별도 로그 파일에 기록&lt;/b&gt;&lt;/span&gt;하기 위해서다.&lt;br /&gt;&lt;br /&gt;또한 파이프(|)를 통해 명령어 A 결과물을 명령어 B 입력으로 넘길 때, A에서 에러가 발생하더라도 정상 출력 통로(fd 1)만 넘어가므로 B프로세스가 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;에러 메시지를 정상 데이터로 오인해 처리하는 연쇄 오류를 방지&lt;/b&gt;&lt;/span&gt;한다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;사용자 권한 모델과 보안&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;리눅스는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;다중 사용자 운영체제&lt;/b&gt;&lt;/span&gt;이다. 권한 모델을 이해하지 못하면, Permission denied 에러 앞에서 막히거나, 반대로 모든 명령어를 sudo로 실행하는 위험한 습관이 생긴다. &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;서버 보안의 기초는 권한 권리에서 시작&lt;/b&gt;&lt;/span&gt;한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;사용자&amp;middot;그룹 모델&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;모든 파일과 프로세스는 소유자(user)와 그룹(group)을 가지며, 권한 검사는 항상 세 단계로 이루어진다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;691&quot; data-origin-height=&quot;257&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beZgWB/dJMcaja9A7P/Y9O34yht0cpkM2plENyWXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beZgWB/dJMcaja9A7P/Y9O34yht0cpkM2plENyWXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beZgWB/dJMcaja9A7P/Y9O34yht0cpkM2plENyWXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeZgWB%2FdJMcaja9A7P%2FY9O34yht0cpkM2plENyWXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;691&quot; height=&quot;257&quot; data-origin-width=&quot;691&quot; data-origin-height=&quot;257&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;도커 컨테이너 내부는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;기본적으로 root로 실행&lt;/b&gt;&lt;/span&gt;된다. 도커 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;컨테이너는 호스트 컴퓨터(실제 서버)의 OS 커널을 공유&lt;/b&gt;&lt;/span&gt;한다.&lt;br /&gt;만약 컨테이너가 해킹 당했는데 내부 프로그램이 root 권한으로 돌고 있다면, 해커가 실제 서버까지 해킹할 수 있다.&lt;br /&gt;&lt;br /&gt;그래서 Dockerfile을 만들 때 USER 지시어를 사용해 최고 권한자가 아닌 일반 사용자를 생성하고, 그 사용자로 프로그램을 실행하도록 명시하는 것이 좋다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Dockerfile은 도커 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;이미지(설계도)를 빌드하기 위한 명령어들을 순서대로 기록한 텍스트 파일&lt;/b&gt;&lt;/span&gt;이다. 수동으로&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; 서버 환경을 구축하는 과정을 하나의 스크립트로 자동화&lt;/b&gt;&lt;/span&gt;한 명세서이다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;보안 원칙&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;권장 패턴&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;✔ 필요한 권한만 부여&lt;br /&gt;✔ sudo로 특정 명령어만 허용&lt;br /&gt;✔ SSH키 권한 600 유지&lt;br /&gt;✔ 서비스 계정 분리&lt;/blockquote&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;지양 패턴&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;✔ 모든 것에 777권한 부여&lt;br /&gt;✔ root로 서비스 실행&lt;br /&gt;✔ 비밀번호 없는 sudo 설정&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>운영체제</category>
      <category>리눅스 #파일시스템 #입출력 #권한모델 #Linux #운영체제</category>
      <author>mins0on</author>
      <guid isPermaLink="true">https://min-soon.tistory.com/60</guid>
      <comments>https://min-soon.tistory.com/60#entry60comment</comments>
      <pubDate>Fri, 22 May 2026 09:15:26 +0900</pubDate>
    </item>
  </channel>
</rss>