Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/main/java/jwt/AuthConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package jwt;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AuthConfig {

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

Choose a reason for hiding this comment

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

👀 Comment

@ConfigurationProperties라는 주입방식을 한번 살펴보고 어떤것이 더 적절한지 생각해보시면 좋겠습니다.

Choose a reason for hiding this comment

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

👀 Comment

저는 이 configuration 이 약간 이질적으로 느껴지는데요.

  1. 왜 auth와 roomescape 패키지는 분리되어야할까?
  2. 왜 jwtUtils는 @bean 어노테이션으로 빈객체 설정이된것일까?
  3. 패키지명이 jwt인 이유는 무엇일까? -> 다른 인증 방법이 생기면 패키지를 또 만들어야하나?

일단 저는 여러모로 이 jwt 패키지 존재 자체가 공감이 잘 안됩니다.
이를 삭제하고 합쳐라 라는 의미의 리뷰는 아닙니다.

혜림이 질문 주신 것 처럼 이를 분리하는 이유를 조금 더 같이 찾아봐야될 것 같은데요.

일단 패키지를 분리하는 것은 생각보다 큰 의미입니다.
설정을 패키지 별로 옵션을 다르게 줄 수도 있겠죠.

사실 저도 현재로선 어떤 활용 방법 자체가 떠오르진 않는데요.
혜림님이 Auth 관련된 영역을 이쪽으로 많이 옮기고, prod, dev, test, ... 환경 별로 auth 설정을 달리하면서 어떤일이 벌어지는지 경험적으로 느끼셨으면 좋겠습니다.

Copy link
Author

Choose a reason for hiding this comment

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

👀 Comment

@ConfigurationProperties라는 주입방식을 한번 살펴보고 어떤것이 더 적절한지 생각해보시면 좋겠습니다.

현재 값이 두개이긴 하지만 더 깔끔하고 관련 설정이 한눈에 보이는 것 같아서 @ConfiguationProperties를 사용해서 수정해보았습니다!

@Value :

단일 값을 주입받기 위해 사용

RelaxedBinding 사용 불가

@ConfigurationProperties :

클래스로 N개의 값을 바인딩하기 위해 사용

RelaxedBinding 사용 가능

주의사항

  • 값의 바인딩을 위해 Setter 필요, 생성자로 바인딩시 @ConstructorBinding을 붙여야 함(스프링 3.0 미만에서만 붙여줌)
  • 해당 어노테이션 사용하기 위해선 @EnableConfigurationProperties에 해당 클래스를 지정해주거나 @ConfigurationPropertiesScan을 통해 스캐닝의 대상이 되도록 해야함

+) RelaxedBinding이란

  • 프로퍼티 값의 이름이 조금 달라도 유연하게 바인딩을 시켜주는 규칙을 의미함.

Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
package roomescape.auth;
package jwt;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import roomescape.domain.member.Member;

@Component
public class JwtTokenProvider {
public class JwtUtils {

@Value("${roomescape.auth.jwt.secret}")
private String secretKey;
@Value("${roomescape.auth.jwt.expiration}")
private long expirationTime;
private final String secretKey;
private final long expirationTime;

public JwtUtils(String secretKey, long expirationTime) {
this.secretKey = secretKey;
this.expirationTime = expirationTime;
}
public String createToken(Member member) {
return Jwts.builder()
.setSubject(member.getId().toString())
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/roomescape/DataLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package roomescape;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import roomescape.domain.member.Member;
import roomescape.domain.member.MemberRepository;

@Component
@Profile("prod")
@RequiredArgsConstructor
public class DataLoader implements CommandLineRunner {

Choose a reason for hiding this comment

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

👀 Comment

혜림이 이번 미션에선 prod라는 profile을 선언해주셨는데요.
이 프로파일로 실행되는 환경은 구체적으로 어떤 환경인가요?
저는 관습적으로 prod 환경이라 하면 실제 고객에게 서비스하는 환경으로 느껴지는데요.

실제 서비스하는 환경이라면, 매번 빌드를 할 때마다 이런 데이터로더가 실행되면 안되지 않을까요?
서비스 데이터는 소중하니까요.
이런 설정은 소위 말하는 dev, local 같은 개발 환경에 더 적합하다고 생각합니다.

Copy link
Author

Choose a reason for hiding this comment

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

말씀하신대로 prod는 실제 서비스 환경이라서 이렇게 매 빌드마다 데이터를 삽입하면 안되겠네요!! dev로 수정하였습니다. 추가로 properties를 yml로 바꿨고 prod, dev, test 환경별로 만들었습니다


private final MemberRepository memberRepository;


@Override
public void run(String... args) throws Exception {
memberRepository.save(new Member("어드민", "[email protected]", "password", "ADMIN"));
memberRepository.save(new Member("브라운", "[email protected]", "password", "USER"));
}
}
2 changes: 2 additions & 0 deletions src/main/java/roomescape/RoomescapeApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan(basePackages = {"roomescape", "jwt"})
public class RoomescapeApplication {
public static void main(String[] args) {
SpringApplication.run(RoomescapeApplication.class, args);
Expand Down
48 changes: 48 additions & 0 deletions src/main/java/roomescape/TestDataLoader.java

Choose a reason for hiding this comment

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

🤔 Request Change

테스트 관련된 코드는 test 패키지로 옮겨주세요.

이번 미션에서 profile을 분리하면서 application 설정을 하는 연습을 하셨을텐데요.
test 또한 환경이고 프로덕션과는 분리될 필요가 있습니다.
환경설정으로 분리될 필요도 있지만 코드상으로도 프로덕션 코드와 테스트 코드를 분리해보면 좋겠습니다.

아마 이 코드를 test영역으로 내리면, 의도와는 다르게 동작할 수도 있을 것 같은데요.
그렇다고 이 코드가 여기 있을 것은 아닌 것 같아요. 그 방법은 그방법대로 찾아보시죠.

Copy link
Author

@HyerimH HyerimH Jul 21, 2025

Choose a reason for hiding this comment

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

아마 이 코드를 test영역으로 내리면, 의도와는 다르게 동작할 수도 있을 것 같은데요. 그렇다고 이 코드가 여기 있을 것은 아닌 것 같아요. 그 방법은 그방법대로 찾아보시죠.

우선 TestDataLoader를 test패키지 하위로 이동하였습니다! 의도와 다르게 작동한다는 걸 제대로 이해하지 못한 것 같아요..!

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package roomescape;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import roomescape.domain.member.MemberRepository;
import roomescape.domain.reservation.Reservation;
import roomescape.domain.reservation.ReservationRepository;
import roomescape.domain.theme.ThemeRepository;
import roomescape.domain.time.Time;
import roomescape.domain.member.Member;
import roomescape.domain.theme.Theme;
import roomescape.domain.time.TimeRepository;


@Component
@Profile("test")
@RequiredArgsConstructor
public class TestDataLoader implements CommandLineRunner {

private final MemberRepository memberRepository;
private final ThemeRepository themeRepository;
private final TimeRepository timeRepository;
private final ReservationRepository reservationRepository;

@Override
public void run(String... args) throws Exception {
Member admin = memberRepository.save(new Member("어드민", "[email protected]", "password", "ADMIN"));
Member user = memberRepository.save(new Member("브라운", "[email protected]", "password", "USER"));

Theme theme1 = themeRepository.save(new Theme("테마1", "테마1입니다."));
Theme theme2 = themeRepository.save(new Theme("테마2", "테마2입니다."));
Theme theme3 = themeRepository.save(new Theme("테마3", "테마3입니다."));

Time time1 = timeRepository.save(new Time("10:00"));
Time time2 = timeRepository.save(new Time("12:00"));
Time time3 = timeRepository.save(new Time("14:00"));
Time time4 = timeRepository.save(new Time("16:00"));
Time time5 = timeRepository.save(new Time("18:00"));
Time time6 = timeRepository.save(new Time("20:00"));

reservationRepository.save(new Reservation("", "2024-03-01", time1, theme1, admin));
reservationRepository.save(new Reservation("", "2024-03-01", time2, theme2, admin));
reservationRepository.save(new Reservation("", "2024-03-01", time3, theme3, admin));
reservationRepository.save(new Reservation("브라운", "2024-03-01", time1, theme2));
}
}
6 changes: 2 additions & 4 deletions src/main/java/roomescape/auth/AdminAuthInterceptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,18 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import roomescape.domain.member.Member;
import roomescape.domain.member.MemberService;
import roomescape.exception.CustomException;

@Component
@RequiredArgsConstructor
public class AdminAuthInterceptor implements HandlerInterceptor {

private final CookieUtil cookieUtil;
private final CookieUtils cookieUtils;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
Cookie[] cookies = request.getCookies();
String token = cookieUtil.extractTokenFromCookies(cookies);
String token = cookieUtils.extractTokenFromCookies(cookies);

if (StringUtils.isBlank(token)) {
throw new CustomException(EMPTY_TOKEN);
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/roomescape/auth/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
public class AuthController {

private final AuthService authService;
private final CookieUtil cookieUtil;
private final CookieUtils cookieUtils;

@PostMapping("/login")
public ResponseEntity<Void> login(@Valid @RequestBody LoginRequest loginRequest,
HttpServletResponse response) {
String token = authService.login(loginRequest);
response.addCookie(cookieUtil.createTokenCookie(token));
response.addCookie(cookieUtils.createTokenCookie(token));
return ResponseEntity.ok().build();
}

Expand All @@ -39,7 +39,7 @@ public ResponseEntity<MemberResponse> checkLogin(HttpServletRequest request,

@PostMapping("/logout")
public ResponseEntity logout(HttpServletResponse response) {
response.addCookie(cookieUtil.createLogoutCookie());
response.addCookie(cookieUtils.createLogoutCookie());
return ResponseEntity.ok().build();
}
}
9 changes: 5 additions & 4 deletions src/main/java/roomescape/auth/AuthInterceptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jwt.JwtUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
Expand All @@ -19,20 +20,20 @@
public class AuthInterceptor implements HandlerInterceptor {

private final MemberService memberService;
private final JwtTokenProvider jwtTokenProvider;
private final CookieUtil cookieUtil;
private final JwtUtils jwtUtils;
private final CookieUtils cookieUtils;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
Cookie[] cookies = request.getCookies();
String token = cookieUtil.extractTokenFromCookies(cookies);
String token = cookieUtils.extractTokenFromCookies(cookies);

if (StringUtils.isBlank(token)) {
throw new CustomException(EMPTY_TOKEN);
}

Long memberId = jwtTokenProvider.extractMemberId(token);
Long memberId = jwtUtils.extractMemberId(token);
Member member = memberService.getMemberById(memberId)
.orElseThrow(() -> new CustomException(INVALID_TOKEN));

Expand Down
5 changes: 3 additions & 2 deletions src/main/java/roomescape/auth/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static roomescape.exception.ErrorCode.INVALID_EMAILPASSWORD;

import jwt.JwtUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import roomescape.domain.member.Member;
Expand All @@ -13,11 +14,11 @@
public class AuthService {

private final MemberRepository memberRepository;
private final JwtTokenProvider jwtTokenProvider;
private final JwtUtils jwtUtils;

public String login(LoginRequest loginRequest) {
Member member = memberRepository.findByEmailAndPassword(loginRequest.email(), loginRequest.password())
.orElseThrow(() -> new CustomException(INVALID_EMAILPASSWORD));
return jwtTokenProvider.createToken(member);
return jwtUtils.createToken(member);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
import org.springframework.stereotype.Component;

@Component
public class CookieUtil {
public class CookieUtils {

private final String cookieName;

public CookieUtil(@Value("${roomescape.auth.cookie.name:token}") String cookieName) {
public CookieUtils(@Value("${roomescape.auth.cookie.name:token}") String cookieName) {
this.cookieName = cookieName;
}

Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ roomescape.auth.jwt.secret= Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=
#Access Token expiration (1h = 3600000ms)
roomescape.auth.jwt.expiration=3600000
roomescape.auth.cookie.name=token

spring.profiles.active=prod
24 changes: 0 additions & 24 deletions src/main/resources/schema.sql

This file was deleted.

2 changes: 2 additions & 0 deletions src/test/java/roomescape/JpaTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import roomescape.domain.member.MemberRepository;
import roomescape.domain.reservation.ReservationRepository;
import roomescape.domain.theme.ThemeRepository;
import roomescape.domain.time.Time;
import roomescape.domain.time.TimeRepository;

@ActiveProfiles("test")
@DataJpaTest
@ContextConfiguration(classes = {TimeRepository.class, ReservationRepository.class, MemberRepository.class,
ThemeRepository.class, RoomescapeApplication.class})
Expand Down
33 changes: 28 additions & 5 deletions src/test/java/roomescape/MissionStepTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,33 @@
import io.restassured.response.Response;
import jakarta.transaction.Transactional;
import java.util.List;
import jwt.JwtUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.stereotype.Component;
import org.springframework.test.annotation.DirtiesContext;

import java.util.HashMap;
import java.util.Map;
import org.springframework.test.context.ActiveProfiles;
import roomescape.domain.reservation.dto.MyReservationResponse;
import roomescape.domain.waiting.dto.WaitingResponse;
import roomescape.auth.LoginRequest;
import roomescape.domain.reservation.dto.ReservationResponse;

import static org.assertj.core.api.Assertions.assertThat;

@ActiveProfiles("test")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
@Transactional
public class MissionStepTest {

@Value("${roomescape.auth.jwt.secret}")
private String secretKey;

@Test
@DisplayName("로그인 성공 시 토큰이 포함된 쿠키를 반환한다")
void 일단계() {
Expand Down Expand Up @@ -60,12 +68,17 @@ public class MissionStepTest {
String token = createToken("[email protected]", "password"); // 일단계에서 토큰을 추출하는 로직을 메서드로 따로 만들어서 활용하세요.

Map<String, String> params = new HashMap<>();
params.put("date", "2024-03-01");
params.put("date", "2024-01-01");
params.put("time", "1");
params.put("theme", "1");

params.put("name", "어드민");

Map<String, String> params2 = new HashMap<>();
params2.put("date", "2024-02-01");
params2.put("time", "1");
params2.put("theme", "1");
params2.put("name", "브라운");

ExtractableResponse<Response> response = RestAssured.given().log().all()
.body(params)
.cookie("token", token)
Expand All @@ -77,10 +90,8 @@ public class MissionStepTest {
assertThat(response.statusCode()).isEqualTo(201);
assertThat(response.as(ReservationResponse.class).name()).isEqualTo("어드민");

params.put("name", "브라운");

ExtractableResponse<Response> adminResponse = RestAssured.given().log().all()
.body(params)
.body(params2)
.cookie("token", token)
.contentType(ContentType.JSON)
.post("/reservations")
Expand Down Expand Up @@ -178,6 +189,18 @@ private String createToken(String email, String password) {

assertThat(status).isEqualTo("1번째 예약대기");
}

@Test
@DisplayName("@Component 없이 JwtUtils가 빈으로 등록되는지 확인한다.")
void 칠단계() {
Component componentAnnotation = JwtUtils.class.getAnnotation(Component.class);
assertThat(componentAnnotation).isNull();
}

@Test
void 팔단계() {
assertThat(secretKey).isNotBlank();
}
}