본문 바로가기
Memo/우테코 4기

[우테코] 장바구니 미션 1~2 단계 학습 로그

by 연로그 2022. 6. 14.
반응형

목차

1. GitHub 저장소

2. 요구사항

3. 학습 사항

4. 피드백

5. 셀프 회고


 


1. GitHub 저장소 🐱‍💻

 

1단계 Repository: https://github.com/yeon-06/jwp-shopping-cart/tree/step1

1단계 Pull Request: https://github.com/woowacourse/jwp-shopping-cart/pull/31

2단계 Repository: https://github.com/yeon-06/jwp-shopping-cart/tree/step2

2단계 Pull Request: https://github.com/woowacourse/jwp-shopping-cart/pull/95

 


2. 요구사항 🚀

 

나인 & 위니의 장바구니 시안

 

1단계 요구사항

  • 회원 관련 CRUD 구현
  • JWT 라이브러리를 활용해 액세스 토큰 발급
  • 토큰을 이용한 인증 구현

 

2단계 요구사항

  • 레거시 코드 리팩터링
    • 레거시 API의 URL에서 customer_name 제거
    • API 접근 시 액세스 토큰으로 인증

 


3. 학습 사항 📚

 

3-1. CORS

 

로컬에서 간단하게 CORS 에러 확인하기

CORS 에러는 Postman을 통해서 확인할 수 없다. 1. 개발자 도구 들어가기 Chrome -> 개발자 도구(F12) -> Console 2. 서버 API 호출 아래 스크립트를 자신의 API 스펙에 맞게 수정해 1에서 실행 fetch("http://lo..

yeonyeon.tistory.com

 

 

[Spring] CORS 에러 해결하기

💥 문제의 시작 Access to fetch at 'http://서버IP:8080/signup' from origin 'API호출한IP' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-C..

yeonyeon.tistory.com

 

 

3-2. 중요한 설정 정보 숨기기

 

OS 환경 변수로 중요한 설정 정보 숨기기

🤗 서론  중요한 설정 정보들이 담긴 파일은 공개되면 안된다. 이를 숨김 처리 하는 방법은 여러가지가 있다. Vault, AWS에서 지원해주는 Systems Manager Parameter Store 등 여러 서비스를 활용할 수 있

yeonyeon.tistory.com

 

 

3-3. NamedParameterJdbcTemplate에서 IN절 사용하기

 

WHERE절 IN과 NamedParameterJdbcTemplate

IN 절을 쓰다 만난 BadSqlGrammarException  Spring은 다양한 종류의 JdbcTemplate을 지원하는데 개인적으로는 NamedParameterJdbcTemplate를 애용하는 편이다. NamedParameterJdbcTemplate에서는 아래 예제처럼..

yeonyeon.tistory.com

 

 

3-4. 테스트 코드로 인한 스트레스 줄이기

 나는 테스트 메서드 이름을 짓는게 너무 스트레스 였다. @DisplayName을 통해 열심히 이름을 생각한 후에 메서드 이름을 영문판으로 또 생각해야한다니.. 테스트에서는 과감하게 한글 이름을 짓기로 했다.

 

@SuppressWarnings("NonAsciiCharacters")
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class NicknameTest {
    // ...
}

 

  • @SuppressWarnings
    • 여러 경고를 무시할 수 있게 해준다.
    • NonAsciiCharacters 옵션을 줘서 한글 메서드를 이름을 지어도 경고가 뜨지 않는다.
  • @DisplayNameGeneration
    • 클래스, 메서드 등의 이름을 변형시켜준다.
    • DisplayNameGenerator.ReplaceUnderscores.class를 통해 _로 표기한 부분은 공백으로 처리된다.

 

🔻 private 테스트 메서드 한글로 짓기

더보기

이름을 잘 지으면 영어로 된 메서드도 이해하기 쉽다. 다만 같은 코드를 놓고 볼 때 한글이 눈에 더 잘 들어온다.

 

메서드 이름 한글판

@SuppressWarnings("NonAsciiCharacters")
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class CustomerAcceptanceTest {

    @Test
    void 회원_가입_비밀번호_빈값으로_인해_실패() {
        Map<String, Object> request = 회원_정보("leo123", "");
        ExtractableResponse<Response> response = 회원_가입(request);
        assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value());
    }
}

 

메서드 이름 영문판

public class CustomerAcceptanceTest {

    @DisplayName("회원 가입 비밀번호 빈값으로 인해 실패")
    @Test
    void sign_up_with_empty_password() {
        Map<String, Object> request = createUserInfo("leo123", "");
        ExtractableResponse<Response> response = signUp(request);
        assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value());
    }
}

 

 

3-5. 자바 기본 라이브러리로 암호화 하기

 spring security를 주입받기 싫어서 java만 사용하는 방법으로 찾아보았다. java.security.MessageDigest라는 클래스를 지원해주어서 단방향 암호화를 편하게 사용할 수 있었다. 다만 이 방식만으로는 보안에 완전히 안전하다고 할 수는 없다. 좀 더 안전한 방식을 알아보고 싶다면 'hash'와 'salt'라는 키워드로 검색해보길 바란다.

 

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class PasswordEncryptor {
    // 암호화해주는 메서드
    public String encrypt(String text) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256"); // or MD5, SHA-1
            md.update(text.getBytes());
            return bytesToHex(md.digest());
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException();
        }
    }

    // 16진수 형태로 변환
    private String bytesToHex(byte[] bytes) {
        StringBuilder builder = new StringBuilder();
        for (byte b : bytes) {
            builder.append(String.format("%02x", b));
        }
        return builder.toString();
    }
}

 

 

 


4. 피드백 💻

🙋‍♀️: 본인(연로그), 🙍‍♂️: 리뷰어(범블비)

 

 

4-1. Interceptor와 Resolver는 Bean이어야할까?

 프로젝트에서 커스텀 Interceptor와 커스텀 ArgumentResolver가 존재한다. 이 둘은 설정 파일에서 Bean으로 등록해주었다. 그런데 리뷰어가 아래와 같은 코멘트를 남겨주었다.

 

🙍‍♂️: 이 두 객체가 꼭 스프링이 관리하는 Bean이여야하는지 한 번 고민해보시면 좋을 것 같습니다.

 

🙋‍♀️: 특별한 의미를 갖고 짠 코드가 아닌데 빈 등록의 의미가..... 없는 것 같아요😅 애초에 addInterceptors / addArgumentResolvers 메서드들이 서버가 구동될 때 단 한번만 호출할거라는 생각이 드네요. 싱글톤으로 관리할 필요도 없고 의존 관계를 주입할 일도 없고 필요 없다고 판단하여 제거했습니다.

 

🔻 AS-IS / TO-BE 코드 비교

더보기

AS-IS

@Configuration
public class AuthenticationPrincipalConfig implements WebMvcConfigurer {

    private final JwtTokenProvider jwtTokenProvider;

    public AuthenticationPrincipalConfig(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(createAuthenticationPrincipalArgumentResolver());
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/customers/**");
    }

    @Bean
    public AuthenticationInterceptor authenticationInterceptor() {
        return new AuthenticationInterceptor(jwtTokenProvider);
    }

    @Bean
    public AuthenticationPrincipalArgumentResolver createAuthenticationPrincipalArgumentResolver() {
        return new AuthenticationPrincipalArgumentResolver(jwtTokenProvider);
    }
}

 

TO-BE

@Configuration
public class AuthenticationPrincipalConfig implements WebMvcConfigurer {

    private final JwtTokenProvider jwtTokenProvider;

    public AuthenticationPrincipalConfig(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new AuthenticationPrincipalArgumentResolver(jwtTokenProvider));
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthenticationInterceptor(jwtTokenProvider))
                .addPathPatterns("/customers/**");
    }
}

 

 

4-2. 커스텀 예외 클래스

🙍‍♂️: 새로운 커스텀 예외 클래스가 추가될 때마다 ControllerAdvice가 변경될 것 같은데 해결할 수 있는 방법이 있을까요? (ControllerAdvice: 예외를 핸들링하기 위해 만든 클래스)

 

🙋‍♀️: 현재 Bad Request를 내려주기 위한 예외, Not Found를 내려주기 위한 예외 등 HTTP 상태 코드에 따라 처리하도록 예외 생성하고 있습니다! 새로운 커스텀 예외 클래스가 추가될 일이 많이 없을 예정이라 생각했어요.

다만 범블비가 말한 문제에 대해서 생각해보다가 어떤 상태 코드를 반환할지 서비스에서 결정한다는 생각이 들었어요. 어떤 예외가 발생한건지 바로 파악하기도 힘든 것 같고요. 그래서 좀 더 구체적인 예외 클래스를 만들고 HTTP 상태 코드에 따른 예외 코드를 상속받도록 리팩터링했습니다! 

 

ex) 패스워드 불일치 예외 처리하기

public class PasswordMismatchException extends UnauthorizedException {
    //...
}

 

예외를 핸들링하는 ControllerAdvice

@RestControllerAdvice
public class ControllerAdvice {
    
    // ...
    
    @ExceptionHandler
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ErrorResponse handleUnauthorized(UnauthorizedException e) {
        return new ErrorResponse(e.getMessage());
    }
}

 

🙍‍♂️: 잘 구현해주셨습니다 💯 모든 커스텀 예외의 부모 클래스를 두고 그 클래스가 메세지 + 상태코드를 가져도 좋을 것 같아요. 상태코드가 없다면 400으로 처리한다던지 하면 일반적으로 처리가 가능할 것 같네요.

 

 

4-3. 패스워드 암호화

 Password라는 VO가 존재한다. 처음에는 Password 생성자에서 encrypt 처리를 했다. 하지만 이 경우 DB에서 불러온 비밀번호 (=암호화된 문자열) 를 Password 객체로 만들때 문제가 생긴다. 생성자에서 암호화를 처리해주기 때문에 원본 비밀번호에 암호화가 2번 처리된 문자열이 된다. 이를 해결하는 방법으로는 여러가지 방법이 있다.

 

  1. 암호화 된/되지 않은 텍스트 구분을 위한 정적 팩터리 메서드 생성
  2. Password는 인터페이스화 시키고 이를 구현한 EncryptPassword, RawPassword 따로 생성
  3. Password에는 암호화된 문자열만 저장
  4. Password에 encrypt() 메서드를 만들고 Password 바깥에서 메서드 호출

 

각자 방법에 장단점이 있다고 생각해서 리뷰어에게 조언을 구했다.

 

🙍‍♂️

  • 현재는 Password 값 객체가 가변이기 때문에 암호화된 값과 평문이 모두 들어갈 수 있는 구조에요. Password 객체를 사용하는 입장에서 객체 내부에 평문이 들어가있을지, 암호화된 값이 들어가있을지 예측할 수 있을까요?
  • Password를 단방향으로 암호화하는 이유는 평문을 보면 안 되기 때문이겠죠. 그래서 Password 객체가 갖고 있어야하는 값은 암호화된 값이라고 생각합니다. 평문은 최대한 빠르게 로직에서 제거하면 좋을 것 같아요.
  • 이렇게 규약을 놓고 보면 정적팩토리 메서드도 나쁘지 않다고 생각합니다. 항상 암호화된 값을 갖는다고 하면 생성자로는 암호화된 값을 받고, 팩토리 메서드 plain으로부터는 평문을 받아서 복호화할 수 있다고 생각해요.

 

👉 결국 내가 선택한건 정적 팩토리 메서드를 생성하는 것이다. 2는 특별한 로직이 없는 단순 VO를 만든건데 너무 과한 설계가 된다는 느낌이 들었다. 3, 4는 raw / encrypt 가 된 상태인지 보장할 수 없는게 큰 결함이라고 생각했다.

 

 

4-4. URI path컨벤션

🙋‍♀️ 다른 크루들과 이야기를 나누다보니 URL에 /api/customers/ ... 식으로 작성하신 분들이 많더라고요 이유를 물어보니 CORS 에러에서 /**와 같이 모든 요청을 허용하는게 보안적으로 문제가 있을 수 있다고 하셨어요 이 사유에 대해서는 납득되었는데 왜 하필 api 키워드를 앞에 붙였는지는 잘 모르겠더라고요😂 URL에도 앞에 뭘 붙여야한다, 어떤 구성으로 만들어야 한다 같은 컨벤션이 있나요??

 

🙍‍♂️ 글쎄요... 백엔드 api를 사용하는 클라이언트에는 웹 뿐만 아니라 ios/android 등 다양한 클라이언트 들이 있는데요, 제가 알기로는 CORS는 웹에만 적용하면 됩니다. 그래서 웹 전용 api에만 붙이면 돼서 굳이 api를 붙일 필요가 있나 싶어요. 연로그가 불필요하다고 생각하신다면 넘어가도 될 부분 같네요 😄

 

 

4-5. 값 객체 이용하기

Customer를 생성할 때 다양한 파라미터를 받고 있다.

// 1 원시 타입 받기
public Customer(Long id, String account, String nickname, Password password, String address) { ... }

// 2 값 객체 받기
public Customer(Long id, Account account, Nickname nickname, Password password, Address address) { ... }

 

🙍‍♂️ 값 객체를 사용해서 얻을 수 있는 이점 중에는 파라미터의 혼동이 없어진다는 점이 있습니다. 연속된 String으로 받는다면 account와 nickname의 순서가 바뀌어도 컴파일러로 통과할 것 같아요. 그래서 전 값 객체를 파라미터로 받는 방식을 유지하면 좋을 것 같은데 연로그의 생각은 어떠신가요?

 

🙋‍♀️ 일단 값 객체에서 String으로 바꾼 이유는 다른 객체에서 도메인으로 전환해야하는 로직 때문입니다. 저는 현재 dto나 entity에서 도메인으로 전환하는 로직을 갖고 있는데 해당 객체들이 도메인의 구성 요소는 무엇인지 하나하나 알고 있는게 마음에 걸렸어요!

다만 범블비가 말씀해주신 것처럼 순서가 바뀌어서 오류를 실제로 겪은 적이 있긴 해요ㅠ_ㅠ 이런 경우에는 빌더 패턴을 적용한다면 파라미터의 혼동이 줄어들지 않을까 싶은데 빌더 패턴에 대해서는 어떻게 생각하시나요?

 

🙍‍♂️ 값 객체가 아닌 원시 타입을 사용하면 외부에서 도메인의 구성 요소가 무엇인지 모르는걸까요? 생성자 파라미터에 타입을 두는 것이 외부에 구성 요소를 노출하는건지도 한 번 생각해보시면 좋을 것 같아요. 저는 값 객체를 사용할 거라면 값 객체에서 값이 꺼내지는 시점은 양 끝 단이어야 한다고 생각해요. 요청이 들어올 때 가장 앞단(dto나 컨트롤러)에서 포장해주고, 어플리케이션에서는 최대한 감싸진 값 객체를 사용하고, 그 값을 꺼내쓰는 시점은 DB에 저장할 때 같은 가장 끝에서 이뤄져야한다고 생각합니다. 연로그의 생각은 어떠신가요? 빌더 패턴을 사용할 수도 있지만 값 객체를 이미 도입하고 사용하고 있으니 값 객체를 사용해도 괜찮을 거 같아요.

 

🙋‍♀️ 범블비 말씀을 계속 곱씹어보니 어떤 타입이 존재하는지 알면 안되는 이유가 없다는 생각이 들기도 해요 (내부 데이터를 노출하지 말자는 말은 불필요한 getter를 남발하지 말자 등에서 쓰이는 말인데 순간 헷갈렸던 것 같아요😅) 범블비의 의견에 동의합니다!


👉 값 객체를 사용하도록 수정

 

 


5. 셀프 회고 😄

 

 이번 미션은 역대급으로 캠퍼스를 정말 많이 나갔다. 주말에도 가고 공휴일에도 가고.. 미션 기간이 2주 조금 넘었나 그 기간 동안 하루 빼고 다 출근했고 그 하루도 동네 카페로 가서 과제했다😭 육체는 정말정말 피곤했지만! 정신은 정말정말 즐거웠다. 팀원들을 잘 만나서 미션 기간 처음부터 끝까지 화기애애한 분위기를 유지했다. 같은 팀인 호호, 에덴, 코린, 위니, 나인 모두모두 좋은 사람이었고 자주 만나고 밥도 먹고 말도 놓고 친해진 것 같아서 기분이 좋았다. 다들 정말 즐거웠어!!! 고맙다!!!!1😍

 

 장바구니 미션 기간 동안 수면 패턴이 약간 변했다. 원래는 기상->우테코->11시쯤 귀가 -> 씻고 방정리 -> 1시 취침 이런 루틴이었는데 반복적이고 단조로운 생활에 스트레스 받았던 것 같다. 조금 피곤하더라도 2~3시쯤 취침하는 대신 자기 전에 뭔가를 하고 잤다. 일기를 쓴다던가 좋아하는 웹툰을 본다던가 게임을 한다던가... 그랬더니 피곤하지만 즐거운 사람이 되었다. 그냥 무기력한 사람인 것보다는 훨씬 좋았다. 저번 학습 로그에 썼던 회고만 해도 스트레스 받아 엉엉 ㅠㅠ 이런 글이었던 것 같은데 이번 미션 기간을 거치며 레벨2는 좋은 기억으로 남을 수 있었다. 

 

 장바구니라는 마지막 미션을 마치고 이제 방학을 맞이했다. 방학식 끝나자마자 데이트도 하고, 몇 개월 만에 친구들 얼굴도 보고, 레벨1 구구조랑 가평도 다녀왔다. 친구들이 내가 맨날 우테코 사람들이랑만 논다고 서운해했는데.. 우테코에 방학이 있는건 인간관계 파탄나지 말라고 주는 기간인게 분명하다... 남은 방학 기간에는 여태 했던 것들을 리마인드하고 테코톡 준비도 좀 해놓고 JPA도 깔짝거려봐야지😄

반응형