2021 06 15
2021-06-15¶
atdd-subway-path 복습하기¶
- 어떤 부분을 중점적으로 개발했는가? - JWT 토큰과 ArgumentResolver/Interceptor를 도입하여 사용자에 대한 인가 처리 - 어느 레이어에서 어떻게 JWT 토큰에 대한 유효성 검사를 진행해야 할까? - 어떤 필드를 토큰으로 가져야 할까? - ArgumentResolver와 Interceptor가 가져야할 적절한 책임 - Interceptor: 토큰에 대한 인증이 필요한 곳에서 Validation을 진행 - ArgumentResolver: Controller의 매개변수로 토큰의 정보를 추출하여 필요한 정보를 담아 만든 객체를 전달해줌
- 고민했던 부분, 느꼈던 감정, 페어 프로그래밍 회고 - 다시 만난 바다와 페어 프로그래밍 - 먼저 공부를 하고 미션을 진행하자는 의견에 동의해줘서 이론 학습 이후 미션 구현에 들어갈 수 있었음 - Spring Web 요청이 들어왔을 때 어떤 관문을 거치는 지 정리해볼 수 있었음 - 스켈레톤 코드에서는 JWT 토큰에 대한 인가 로직이 AuthService에 포함되어 있었음 - ArgumentResolver에 AuthService를 주입해주는 형식으로 코드를 작성해야 함 - 이게 어색하다고 느꼈음 - ArgumentResolver가 Controller에 들어가기 전에 거치는 단계라고 생각함 - Controller에서 Service를 호출하는게 맞지 않나? - 따라서 JwtTokenProvider를 ArgumentResolver에 주입하여 활용하기로 함 - 코니의 리뷰가 상당히 꼼꼼함
- 피드백 공유 및 코드가 개선된 포인트 공유
- ResponseEntity에 제네릭 타입 지정해 줄 것
- 없으면 Void로 지정
- 토큰 유효성 확인을 어디서 하는 것이 좋을까?
- InterceptorHandler를 통해 DispatcherServlet이 컨트롤러 호출하기 전에 요청 가공해주자
- ArgumentResolver를 통한 확인을 하려면, 토큰 확인이 필요한 컨트롤러 메서드 마다
@AuthenticationPrincipal이 필요함 - 얘는 토큰에서 필요한 정보를 추출해서 필요한 파라미터 넘겨주기 위함에 더 가깝다 - 토큰에는 민감하지 않은 사용자 정보를 넣어두기로 하자 - JWT 토큰 같은 경우, header와 payload는 그냥 BASE64 인코딩에 불과 - 토큰이 유출되면 해당 값들은 그냥 해독이 가능함 - 혹시나 유출되도 민감하지 않은 정보를 payload에 넣어두는 것이 좋아보임 - 하지만 JWT 토큰의 강점은 토큰 자체에 정보를 넣어둘 수 있어, DB를 매번 찌를 필요 없이 토큰의 정보를 조회해 사용할 수 있다는 것인데... > 사실 JWT의 큰 장점 중 하나가 토큰 자체에 정보를 담을 수 있다는 건데,
민감 정보 다 빼고 나면 결국 토큰이 담은 정보만으로는 부족해서 검증 시마다 인증 서버 또는 DB 조회가 불가피해질 수도 있죠
서비스 특성에 따라 트레이드오프를 고려해 어떤 정보를 담을지, 만료 기간은 어떻게 가져갈지,
리프레시는 어떻게 할지 등을 잘 결정해서 사용하는 게 중요한 것 같아요.- 해당 사이트에서 https://jwt.io/ 내가 발급 받은 토큰 + signature 돌리는데 왜 안되냐... 😅- Interceptor에서 토큰에 양식에 대해서만 Validation을 진행했지, 따로 토큰 필드를 통해 DB 조회를 하지는 않았음 - JWT 토큰 validation에서 중요한 건 secretKey와 만료 날짜 (Payload를 추출하여 DB 조회를 따로 해주지 않았음)
java public boolean validateToken(String token) { try { Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); return !claims.getBody().getExpiration().before(new Date()); } catch (JwtException | IllegalArgumentException e) { return false; } }- 만약 이렇다면, 회원가입 바로 하고 토큰 받고 탈퇴하면 이건 유효한 토큰으로 간주되겠지? - 인가된 사용자에 한에서만 서버에 자원 생성등의 요청을 허락해줄라고 했는데... - 탈퇴한 사용자가 이전에 발급받은 토큰을 가지고 서버의 자원에 접근하고 CRUD 로직 수행하면 안될텐데 말이야- 커스텀 예외를 만들 때, 알맞는 Layer의 예외를 상속받을 것 - Service Layer에서 던져줄 커스텀 예외를
org.springframework.dao.EmptyResultDataAccessException이걸 상속받음 - 예외 자체에@ResponseStatus를 정의하는 것 보다@ExceptionHandler를 통해 예외 잡고 ResponseEntity 반환하는 것 추천 -@ResponseStatus를 사용하면 HttpServletResponse.sendError 메서드가 사용됨 - 참고: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/ResponseStatus.html - 해당 메서드를 호출한다는 것은 Response가 완료되었다 간주하고 더 이상 추가적인 정보를 부여할 수 없다는 뜻 - 또한 Servlet Container는 HTML 에러 페이지를 반환하기 때문에 REST API 설계와 어울리지 않는 예외처리 방식임 -@SQLException에 대한 처리를@ControllerAdvice에서 하는게 맞을까? - Dao에서 발생할 수 있는 예외를 Service에서 잡아, 의미있는 RuntimeException으로 변경해주는 것이 좋을 듯 하다 - 기타 사항 - 도메인을 응집도 높게 처리하자 - 객체에 메시지를 보내 로직 구현할 것 - 메서드를 만들 떄 오버로딩을 통해 구분하지 말고, 메서드 명으로 확실히 드러내자 - validation을 통해 필요한 필드를 검사했다면 이를 반환하도록 변경해볼 것
- 개선될 포인트 - [PathService에 SubwayMap 도메인 객체를 위한 전처리 로직이 있음] - [Spring Web MVC 요청 절차에 대한 정리하기] - [JWT 토큰 정리하기] - 참고: https://blog.outsider.ne.kr/1160?commentId=570298#comment570298 - 토큰 자체가 정보를 들고 있어, 서버를 무상태로 유지할 수 있음 - Header와 Payload는 Base64 Encoding만 되어있어 탈취당한다면 그냥 까볼수 있음 - 중요한 정보가 들어가면 안되는 이유 (최소한의 claim set만 담을것) - Claim Set의 내용이 많아지면 토큰의 길이가 길어짐 - 너무 많은 정보를 담지 말자 - 토큰을 강제로 만료시킬 방법이 없음 - 클라이언트가 로그아웃 하더라도, 해당 토큰 자체가 만료되는 것은 아님 (내가 로컬 스토리지에서 지워주더라도) - [xss, csrf 등 웹 보안 조금 공부하기] - https://velog.io/@0307kwon/JWT%EB%8A%94-%EC%96%B4%EB%94%94%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C-localStorage-vs-cookie
atdd-subway-fare 복습하기¶
- 어떤 부분을 중점적으로 개발했는가?
- 요금 정책 기능 구현 방법 - 요금 정책 관리 - FarePolicy와 같은 도메인을 따로 만들어 if 분기로 관리할 지 - FareByDistance, FareDiscountByAge와 같은 Enum으로 관리할 지 <-- 채택 - 요금을 계산하는 로직 - FareCalculator라는 도메인을 따로 만들어 계산 시마다 도메인 객체 생성 및 사용 - FareCalculator라는 서비스 클래스를 싱글톤으로 등록하여 해당 빈 객체를 사용 <-- 채택 - 프론트 측의 요청에 따른 추가 API 구현 - GET: /stations/transfer - POST: /members/email-check - 예외 처리를 어떻게 할 것인가? - 기능별 패키지에 Exception 패키지를 만들어 해당 기능에서 발생할 예외를 커스텀 예외로 구현 - 기능별 패키지 controller에 기능ControllerAdvice라는 클래스에서 해당 기능에서 발생하는 예외를 핸들링해줌 - ErrorResponse 클래스를 만들어 예외 발생시 응답할 양식을 통일
- 고민했던 부분, 느꼈던 감정, 페어 프로그래밍 회고
- 파피와 아주 야무진 페어프로그래밍
- 앞선 배포 관련 https 설정, nginx 설치 등이 재미있었음
- ExceptionHandler를 기능별로 둘지, 아니면 전역에서 하나로 퉁칠지 고민함
- ArgumentResolver에서 Optional을 반환
- 지하철역 노선 금액 조회 기능 같은 경우 로그인한 사용자가 있어도 되고, 없어도 됨
- 이를 구현하고자 Optional
를 반환 - "필수로 있어야할 컨트롤러의 매개변수를 사용자의 요청을 검증하여 넣어준다"는 ArgumentResolver의 역할이 퇴색됨 - 인수테스트만으로 충분한가? - Controller에 대한 단위테스트를 진행하려고 했는데 문제가 있었음 (결과적으로는 진행하지 않았음) - 서비스가 GET 요청을 제외하고는 모두 로그인이 되어있어야 요청 가능하도록 설계했음 - 그러다 보니, Controller 테스트를 위해 로그인을 해서 유효한 JWT 토큰을 발급 받고, - 이를 헤더에 넣어주어 MockMvc로 요청을 보냈어야 했음 - 이렇게 보니 인수테스트와 다를게 없어보였음 - Presentation Layer인 Controller를 테스트하는 것은 어쩌면 인수테스트에서 자연스레 검증된 것이라 생각도 들었음 - HttpServletRequest등을 Mocking하는 것도 생각해봤는데... 테스트하려는 로직보다 Mocking에 더 많은 노력이 들어가는 것 같은데... - Validation을 RequestDto와 Domain에서 어느 정도로 해줘야 하는가? - Domain에서 validation 열심히 해주는 것을 Level1때 배움 - Framework에 의존적이지 않은 도메인 설계가 가능했음
- 피드백 공유 및 코드가 개선된 포인트 공유
- Optional
대신 다형성을 도입하여 LoginUser에 대한 해결 - LoginUser extends User, AnonymousUser extends User - 인수테스트 말고 도메인 테스트들도 작성하자 - 기능의 추가/수정에 대한 빠른 피드백 받기 위함 - 확장성이 필요하지 않다고 생각하는 어플리케이션에서는 적절히 책임 나눠서 Validation 진행 - 앞단에서 진행한 Validation 믿고 뒤에꺼 쭉 진행 - 어플리케이션의 성격과 중요성에 따라 적당히 타협을 보자 - NamedParameterJdbcTemplate을 사용해볼것 - 쿼리에 직접 어떤 key값이 대응될 지 지정해 줌 public void deleteById(Long id) { String sql = "delete from STATION where id = :id"; namedParameterJdbcTemplate.update(sql, Collections.singletonMap("id", id)); }- Line등의 도메인의 이름을 따로 객체로 포장하기 - 원시값 포장 - 분리될 만큼의 책임이 있다고 생각 안했음 - 근데 오히려 분리하고 나니 Line의 로직들이 깔끔해져서 보기 좋음 - 분리될 만큼의 책임 != 로직이 있어야 함 - 유지보수확장에 유리한 코드를 짜기 위해 객체지향 설계 하는 거면, 잘게 나누는 것이 좋을 수도 있겠다 생각함 - RequestDto에 primitive type이 있으면 누락 시 default 0이 들어갈 수도 - 애초에 Null을 받을 수 있는 래퍼 클래스를 사용하고 +
@NotNull및@PositiveOrZero등의 Bean Validation 추가해주자 - update 로직을 처리할 때 특정 필드만 바꾼 새로운 객체를 만들지 VS 기존 객체의 값들을 바꿔줄지 - 전자: 도메인 객체의 불변을 보장, but 도메인에 필드가 추가되면 유지보수해줘야할 포인트들이 줄줄이 생김 - 후자: 기존 객체를 조금 더 존중하는 느낌, 너가 바뀌니까 너의 필드값을 바꿔줄게 but 불변성을 보장할 수 없음 - 기타 사항 - 매직넘버 상수화 - 디미터 법칙 준수
- 개선될 포인트
나의 PR을 소개합니다 Subway-Path¶
- 포모
- Resolver와 Interceptor의 적절한 활용
- 외부 라이브러리인 WeightMultiGraph을 도메인에서 가지고 있어 매번 생성하는 것은 비추
- Graph를 세팅해주기 위해
1. Line이 수정될 때 마다 캐싱해둠
2. 이벤트 리스너 두고 Line 수정시 graph 변경
-
@EventListener가 변경될 때 해당 코드가 호출되는 형식
- 우기
- DTO 검증을 Spring Validation으로
- Resolver와 Interceptor의 역할 분리
-
@DirtiesContext의 테스트 독립성 보장 문제 - BEFORE_EACH_TEST_METHOD - AFTER_EACH_TEST_METHOD
- 완태 - Interceptor는 권한 자체를 검증, ArgumentResolver는 필요한 필드를 추출하여 컨트롤러에 넘겨줌 - NamedParamJdbcTemplate 도입 - Path를 Bean으로 등록하여 사용하기 - 외부 라이브러리를 어디에 두는게 좋은가?
- 느낀점
- 라이브러리를 가져와서 너무 아무생각없이 서비스에 다 박아놓은 듯한 코드
- 어디에 두는 것이 더 좋을까 고민해본적 있어?
-
@EventListener는 뭘까?
나의 PR을 소개합니다 Subway-Fare¶
- 포모
- Java5의 가변인자 로직의 등장으로 JdbcTemplate.update(sql, new Object[]{"감싸줄 필요 X"})
- Exception의 계층적 구조를 abstract/final로 나타내자
- Spring의 JdbcTemplate 도 다음과 같이 예외를 처리하고 있음
public class InvalidDataAccessApiUsageException extends NonTransientDataAccessException { public InvalidDataAccessApiUsageException(String msg) { super(msg); } public InvalidDataAccessApiUsageException(String msg, Throwable cause) { super(msg, cause); } } public abstract class NonTransientDataAccessException extends DataAccessException { public NonTransientDataAccessException(String msg) { super(msg); } public NonTransientDataAccessException(@Nullable String msg, @Nullable Throwable cause) { super(msg, cause); } } public abstract class DataAccessException extends NestedRuntimeException { public DataAccessException(String msg) { super(msg); } public DataAccessException(@Nullable String msg, @Nullable Throwable cause) { super(msg, cause); } }- 기능별 패키지가 MSA 구조로의 변경이 수월함 - Exception도 패키지 별로 가지는 게 좋을 듯함 - Fare라는 기능이 어느 패키지에 들어가야 할 지 애매하다고 느낌 - 그냥 따로 Fare 패키지로 분리함 -
@Transactional(readOnly=true)- JPA 구현체로 Hibernate 사용시 트랜잭션 readOnly 사용추천 - flush 모드를 NEVER로 설정해 해당 트랜잭션 안의 더티 체킹 X - 더티체킹: JPA가 트랜잭션 끝나는 시점에 변화가 있는 모든 엔티티 DB에 자동반영 - 데이터베이스와 sync를 맞춰주지 않아도 되나본데...?
- 우기
- 인수테스트만 작성된 상태에서 중복을 빼고 어디까지 테스트해야하는가?
- Controller는 Service를 모킹하여 인수테스트
- Service는 통합테스트
- Repository / Domain은 단위테스트
-
@Nested를 통해 테스트를 깔끔히 만든것이 인상적 -@Mock: 해당 객체로 직접 테스트를 실행할 수 있음 -@MockBean:@SpringBootTest의 Bean을 MockBean으로 덮어씌움
- 완태 - 도커와 PC가 동신할 때 Docker0를 통해 통신함 - 참고: https://phantasmicmeans.tistory.com/entry/Docker-Container-Network%EC%97%90-%EB%8C%80%ED%95%9C-%EC%9D%B4%ED%95%B4 - Nginx가 도커 내에서 돌아가고 있어 local에 있는 8080 찌르라면 172.17.0.1 - 도커 내의 로컬호스트가 172.17.0.1 - 데코레이터 패턴을 통해 거리/나이 금액 산정 처리 - jdbcTemplate.update()를 통해 확인되는 affectedRow 갯수로 예외 처리하는 방법
- 느낀점 - https, ssl, nginx등 인프라 공부 조금 더해보기 - 예외 처리 전략에 대한 고민한 것을 정리해보기