Skip to content

[그리디] 김민욱 SpringCore 7~9단계 제출합니다. - #274

Open
hapdaypy wants to merge 69 commits into
next-step:hapdaypyfrom
hapdaypy:hapdaypy5
Open

[그리디] 김민욱 SpringCore 7~9단계 제출합니다.#274
hapdaypy wants to merge 69 commits into
next-step:hapdaypyfrom
hapdaypy:hapdaypy5

Conversation

@hapdaypy

@hapdaypy hapdaypy commented Jul 27, 2026

Copy link
Copy Markdown

소개

안녕하세요 다빈 리뷰어님 ~
세종대 그리디 4기 백엔드 김민욱입니다!

깊은 내용이 꼭 아니더라도 키워드나 방향성만 제시해 주신다면, 스스로 공부를 이어나갈 수 있습니다 !

PR 본문은 다음과 같이 구성되어 있습니다.

  • 단계별 구현 사항: 각 단계별로 구현한 사항을 적어두었습니다.
  • 셀프 리뷰: 미션의 요구사항 외의 것에 대해서 스스로 고민해보고 적용해본 결과를 적어두었습니다.
  • 학습 내용 정리: 미션의 요구사항내에서 미션에 관한 키워드에 대해서 의심해보고 학습하고 이해한 내용을 적었습니다.
  • 의논사항: 정답이 없을 것 같은 주제에 대해서 리뷰어님과 의논하고 싶은 사항을 작성하였습니다.

리뷰어님께서 시간이 없으시다면, 셀프 리뷰를 바탕으로 부족한 부분등에 대해서 자유롭게 리뷰를 남겨주시면 감사하겠습니다!


단계별 구현 사항

7단계

  • JWT 관련 로직을 roomescape와 같은 계층의 jwt 패키지로 분리
  • Access Token 인증 과정에서 불필요한 DB 접근 최소화

8단계

  • schema.sql 대신 데이터베이스를 초기화하는 클래스 생성
  • 프로파일에 따라 다른 초기 데이터를 등록하도록 분리

셀프 리뷰

사용자 정보 변경을 새로 발급하는 Access Token에 반영할 수 있는가?

기존 방식은 Refresh Token 안에 저장된 사용자 ID와 권한을 그대로 이용해 새로운 Access Token을 만들었습니다.

예를 들어, Refresh Token에 다음 정보가 저장되어 있습니다.

{
  "id": 1,
  "role": "ADMIN"
}

DB에서 해당 사용자의 권한을 ADMIN에서 USER로 변경해도 기존 Refresh Token에는 계속 ADMIN 권한이 들어 있습니다. 따라서 Refresh Token의 정보만 사용해 Access Token을 재발급하면 새로운 Access Token에도 ADMIN 권한이 들어갈 수 있습니다.

이것이 기존 코드의 문제점이라고 생각하였습니다.

따라서, 사용자 정보 변경을 재발급된 Access Token에 반영하기 위해, Refresh Token으로 Access Token을 재발급할 때 DB에서 회원을 다시 조회하도록 리팩터링했습니다.

public String refreshAccessToken(String refreshToken) {
    LoginMemberInfo tokenMember = parseRefreshToken(refreshToken);

    Member member = memberDao.findById(tokenMember.id())
            .orElseThrow(AuthenticationException::new);

    return createAccessToken(member);
}

이렇게 하면 회원의 이름, 이메일, 권한이 변경되거나 회원이 삭제됐을 때 DB의 최신 상태를 확인할 수 있습니다.

이미 발급된 Access Token의 정보가 즉시 변경되는 것은 아니지만, Refresh Token을 이용해 Access Token을 재발급하는 시점에는 DB에서 조회한 최신 사용자 정보를 반영할 수 있도록 설계했습니다.


학습 내용 정리

@Configuration과 빈 등록은 DB 접근 최소화와 어떤 관계가 있는가?

문제의 요구 사항에서

JWT 관련 로직을 roomescape와 같은 계층의 auth 패키지의 클래스로 분리하세요.
불필요한 DB 접근을 최소화 하세요.

이 두가지와 @configuration 가 무슨 상관인지 이해가 되지 않았었습니다.

하지만 결과적으로

불필요한 DB 조회를 최소화 할 수 있는 이유는 @Configuration이나 @Bean를 활용하여,
JWT 파싱 책임을 JwtUtils로 분리하고, AuthUserArgumentResolver가 회원을 다시 조회하지 않고 Access Token의 정보를 사용하도록 의존 관계를 변경할 수 있기 때문에 DB 접근을 최소화 할 수 있음을 학습하였습니다.

기존 코드는 JwtUtils@Component로 등록되어 있어서 특정 비즈니스 로직과 강하게 결합되어 있는 형태였습니다.
JWT 토큰 하나를 확인하기 위해 불필요한 DB 조회까지 실행될 수 있다고 합니다.

JwtUtils는 JWT 생성과 파싱만 담당하는 것으로 스스로 정의했기 때문에 Spring이나 회원 저장소에 직접 의존할 필요가 없습니다. 이에 따라 JwtUtilsroomescape 패키지 외부의 jwt 패키지로 분리했습니다.

외부로 분리한 뒤에는 @Configuration@Bean을 사용해 JwtUtils를 명시적으로 빈으로 등록했습니다.

@Configuration
public class JwtConfig {

    @Bean
    public JwtUtils jwtUtils(
            @Value("${roomescape.auth.jwt.secret}") String secretKey
    ) {
        return new JwtUtils(secretKey);
    }
}

이렇게 하면 JwtUtils는 JWT 생성과 파싱에만 집중하고, Spring의 컴포넌트 스캔이나 회원 관련 비즈니스 로직과 직접 결합되지 않습니다.

AuthUserArgumentResolver가 Access Token을 읽을 때 MemberServiceMemberDao를 호출하지 않고, 토큰에 포함된 회원 ID와 권한 정보를 바로 사용하도록 의존 관계를 변경했기 때문에 불필요한 DB 조회가 사라졌습니다.

Access Token 추출
→ JwtUtils로 토큰 검증 및 파싱
→ 토큰에 포함된 회원 ID와 권한 확인
→ LoginMemberInfo 생성
→ DB 조회 없이 인증 완료

다만 Refresh Token으로 새로운 Access Token을 발급할 때는 변경된 회원 정보를 반영하기 위해 의도적으로 DB를 조회하도록 했습니다.


DB 초기화와 CommandLineRunner의 관계

기존 프로젝트에서는 schema.sql을 사용했습니다.

INSERT INTO member ...
INSERT INTO theme ...
INSERT INTO reservation ...

현재 프로젝트는 하나의 schema.sql에서 초기 데이터를 모두 관리하고 있었기 때문에 실행 환경별 데이터를 구분하기 어려웠습니다. 테이블이나 엔티티 구조가 변경되면 SQL도 직접 수정해야 했습니다.

이를 개선하기 위해 CommandLineRunner를 구현한 Loader에서 Repository를 통해 초기 데이터를 저장하도록 변경했습니다.

memberDao.save(new Member(...));
themeDao.save(new Theme(...));

초기 데이터는 프로파일에 따라 구분했습니다.

  • DataLoader: test 프로파일을 제외한 일반 실행 환경의 회원 데이터 등록
  • TestDataLoader: 테스트에 필요한 회원, 테마, 시간, 예약 데이터 등록
  • @Profile을 사용해 실행 환경에 맞는 Loader만 등록
@Component
@Profile("!test")
public class DataLoader implements CommandLineRunner {
    // ...
}
@Component
@Profile("test")
public class TestDataLoader implements CommandLineRunner {
    // ...
}

테이블 생성은 Loader가 아니라 Hibernate가 담당합니다.

Hibernate가 JPA 엔티티를 기준으로 테이블을 생성하고, Loader는 생성된 테이블에 초기 데이터를 저장합니다.

memberDao.save(member);

내부적으로는 다음과 비슷한 SQL이 실행된다고 합니다.

INSERT INTO member (name, email, password, role)
VALUES (?, ?, ?, ?)

마무리

소중한 시간을 내어 리뷰해 주시는 만큼, 남겨주신 내용을 바탕으로 이번 주 학습 내용을 더 깊이 정리해 보겠습니다.

mgim9316-a11y and others added 30 commits June 29, 2026 13:08
hapdaypy added 29 commits July 13, 2026 14:43

@70825 70825 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 민욱님~ 다시 뵙네요 잘부탁드려요~!! 🙇‍♂️

PR 본문에 내용 잘 적어주셔서 민욱님이 어떤 방향으로 생각하고 공부하셨는지 잘 이해가 됐어요. 관련해서 저도 본문에 추가로 내용 적어둘게요

추가적으로 민욱님 리뷰 보면서 느낀건데 미션 진행하면서 궁금한 부분도 추가로 적어주시면 좋을 것 같아요!! 이미 궁금한걸 다 찾아보고 해결하신거라면 어쩔 수 없지만 😄 호옥시 있으면 적어주시길 바래요

코드 리뷰의 경우에는 이전 미션 마지막 커밋을 참고해서 여기 범위만 봤었는데 혹시 아니라면 코멘트 부탁드려요~


따라서, 사용자 정보 변경을 재발급된 Access Token에 반영하기 위해, Refresh Token으로 Access Token을 재발급할 때 DB에서 회원을 다시 조회하도록 리팩터링했습니다.

오호 빈틈 없는 설계 좋습니다 👍
그런데 궁금한 부분이 있는데 /token/refresh를 통해 재발급을 하는 것 같은데, 현재 서버 실행하면 사용되는 곳은 없는거죠?? 만약 정보 변경 기능이 생긴다면 해당 API를 요청하는거로 이해했는데 맞을까요?

다만 Refresh Token으로 새로운 Access Token을 발급할 때는 변경된 회원 정보를 반영하기 위해 의도적으로 DB를 조회하도록 했습니다.

요거는 저도 민욱님이 생각한대로 잘 이해하고 만드신 것 같아요~

CommandLineRunner

👍👍

Comment on lines +12 to +13

public class JwtUtils {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기는 따로 빈 등록을 하지 않은 이유가 있나요??

Comment on lines +64 to +69
return new LoginMemberInfo(
claims.get("id", Number.class).longValue(),
claims.get("name", String.class),
claims.getSubject(),
MemberRole.from(claims.get("role", String.class))
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거는 LoginMemberInfo에 정적 팩토리 메서드를 만들어서 관리하는건 어떻게 생각하시나요?
정팩메로 안에 넣어주면 조금이나마 더 깔끔한 코드가 나올 것 같아서요!

Comment on lines +18 to +20
@Component
@Profile("test")
public class TestDataLoader implements CommandLineRunner {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트에서 사용하는 클래스는 테스트 패키지쪽에 넣어두는게 더 목적에 맞게 배치해두는 것 같다고 생각하는데 어떻게 생각하시나요??

Comment on lines +3 to +4
spring.datasource.url=jdbc:h2:mem:database;DB_CLOSE_ON_EXIT=FALSE
spring.jpa.hibernate.ddl-auto=create-drop

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • DB_CLOSE_ON_EXIT=FALSE
  • spring.jpa.hibernate.ddl-auto=create-drop

위 옵션은 각각 어떤 역할을 하게 되는 것일까요?

@Component
@Profile("!test")
public class DataLoader implements CommandLineRunner {
private final MemberDao memberDao;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기는 Dao로 네이밍을 정하게된 이유가 있나요??
다른 분들 보면 Repository로 네이밍을 정해두는데 민욱님은 Dao로 해두셔서 이유가 궁금해요


@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
@ActiveProfiles("test")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기는 테스트 코드 파일이 여러개 생긴다면 매번 클래스 위에 @ActiveProfiles("test")를 붙여야해서 불편할 수도 있는데요
test에도 application.properties 파일을 추가한다면 @ActiveProfiles("test")를 붙이지 않아도 될 것 같은데 한 번 수정해보시겠어요?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants