끊임없이 검증하라

나에게 당연할지라도

Project

P1_클론 프로젝트(feat. 스프링부트와 AWS로 혼자 구현하는 웹 서비스)_5-2

fadet 2022. 3. 30. 17:44
* 이 포스트는 전 배달의민족, 현재 인프런에 계시고 유튜브 개발바닥의 크리에이터이신 개발자 이동욱님의 '스프링부트와 AWS로 혼자 구현하는 웹 서비스'를 기반으로 작성되었음을 알립니다. 포스트 맨 아래에 관련 링크가 있습니다. 책의 내용을 기반으로 작성되기에 실습 중이라면 책을 main 해당 포스트를 sub로 참고해주세요. 책의 설명이 부족한 부분 위주로 포스트가 구성됩니다.

5.4 어노테이션 기반 개선

 

# LoginUserArgumentResolver

여기서 resolver에 대해서 어색할 수도 있습니다. 스프링을 배우신 분이라면 viewResolver를 통해 어떤 방식인지 잘 아시겠지만 잘 모르시는 분들을 위해 쉽게 말하자면 일종의 정거장 개념이라고 생각하시면 됩니다. 사람의 목적지에 따라 port를 나누고 차에 태우듯 파라미터를 판단하고 세션으로 전달합니다. 

public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {

    private final HttpSession httpSession;

    @Override
    // parameter를 판단하기에 return타입이 boolean임
    public boolean supportsParameter(MethodParameter parameter) {
        // 1 어노테이션 확인
        boolean isLogiUserAnnotation = parameter.getParameterAnnotation(LoginUser.class) != null; 
        // 2 class가 Session.User인지 확인
        boolean isUserClass = SessionUser.class.equals(parameter.getParameterType());
        // 1 and 2 가 만족할때만 true return
        return isLogiUserAnnotation && isUserClass;
    }

    @Override
    // *
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        return httpSession.getAttribute("user");

마지막 resolveArgument 메소드는 책에 나온 아래 코드의 역할을 그대로 수행합니다.

SessionUser user = (SessionUser) httpSession.getAttribute("user");

# WebConfig

다음은 책에 나온 내용처럼 WebConfig에 위에 작성한 resolver를 등록합니다. viewResolver든 argumentResolver든 이는 개발자가 추가한 클래스기에 설정 정보인 스프링 Config에 등록되어야 사용 가능합니다. 

 

5.5 세션 저장소를 DB로 구성

 

# application.yml

책의 내용대로 진행해서 문제가 없다면 쉽게 넘어갈 수 있는 부분입니다. 하지만 h2 db에 테이블이 생성되지 않는 사람이 존재할겁니다. application.yml에 앞서 jpa에서 테이블을 drop한 뒤 생성해줬던 옵션인 ddl-auto와 같이 테이블 생성에 대한 옵션인  jdbc.initialize-schema를 always로 추가해줍니다. 

session:
  store-type: jdbc
  jdbc.initialize-schema: always

이렇게 설정해도 테이블이 생성되지 않는다면 쿼리를 작성해 직접 테이블을 생성해줘야 합니다.(...)

 

5.6 네이버 로그인

 

# 네이버 오픈 api

해당 파트의 자세한 절차는 책 그대로 진행하시면 됩니다. 전체적인 순서만 언급하고 중요한 내용만 살펴보겠습니다.

> 네이버 개발자 센터 접속

> 어플리케이션 등록

> API 서비스 환경 설정

- 서비스 URL은 우리가 개발을 진행하는 URL을 입력하면 되는데 여기서 설정하는 Callback URL을 잘 입력해야합니다. 이 URL은 이후 application.yml의 redirect-uri와 일치해야합니다.

 

# application.yml

properties를 사용하신다면 책의 내용대로 진행하시겠지만 yml을 사용하면 정말 주의해야하는 부분입니다. yml을 입력해야할 때 주의해야하는 공백도 있지만 yml파일은 '/'를 반드시 따옴표로 감싸주어야 인식합니다.

naver:
  client-id: pg5tfV2q7nS_NNyvhQsQ
  client-secret: JgzIbxAayY
  redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
  authorization-grant-type: authorization_code

위에서 언급했듯 redirect-uri는 콜백 url과 일치해야만 합니다. 설정 부분이 나올때마다 강조하는 이유는 인텔리제이 커뮤니티 버전에선 설정 파일에 대한 자동완성을 지원하지 않습니다! 플러그인을 깔 수 있긴 하지만 버전 관리 등 설정이 또 복잡합니다. 그렇기 때문에 다른데서 코드가 좀 틀려도 컴파일 에러가 발생하여 조금 고생하면 해결 가능하지만 설정 문법 오타의 경우 에러가 나면 직접 육안으로 몇 시간을 봐도 잡히지 않는 시간 낭비를 할 수가 있습니다. 저의 경우 yml은 똑바로 작성했는데 네이버 API URL에 ':' 하나를 빼먹어 2시간을 날렸습니다.(...) 어쨋든 정말 컴파일로 잡아줄 수 없는 부분은 항상 조심 또 조심해야 합니다.

 

5.7 테스트에 시큐리티 적용하기

 

# 시작 전

기존 테스트 코드는 이제 더 이상 작동하지 않습니다. 책을 보면 알겠지만 현재 어플리케이션은 로그인하면 인증된 사용자기에 권한에 대한 문제를 해결할 수 있지만 기존에 작성한 테스트코드는 현재 아무런 인증 정보가 부여되지 않았기때문에 인증 관련 테스트는 모조리 실패할 것입니다. 앞으로 만날 다른 프로젝트의 테스트도 시큐리티의 적용은 필요할 것이니 어려운 부분을 체크하며 익히길 바랍니다.

 

당연히 모든 수정은 단계적으로 진행합니다. 단계적 수정이 아닌 한번에 모든 코드를 고치려는 시도는 본인이 자신있다면 상관은 없겠지만 빌드 과정 중 버그 내포의 시작점일 수 있으며 수정 후 코드가 제대로 작동하지 않을 가능성이 큽니다. 단계를 명시하기 때문에

 

# 테스트 코드 수정(Number로 구분하는게 적절하다 판단하기에 #구분은 여기서 사용하지 않습니다)


1 No found CustomOAuth2UserService

- 소셜 로그인 관련 설정값들이 main이 아닌 test에는 설정되지 않았기 때문입니다. test에도 동일하게 application.yml을 추가해주고 주의해야할 것은 테스트 사용자 정보만 쓸 것이기에 oauth에 대한 yml파일을 추가할 필요 없이 한 파일에 때려 넣어도 무방합니다.

// 상위 prefix 생략 코드
        registration:
          google:
            client-id: test
            client: test
            scope: profile, email

2 시큐리티 테스트를 위한 임의의 인증된 사용자를 추가합니다.

- post 등록 test를 실패하는 로그를 보면 인증되지 않은 요청은 이동시킵니다. 따라서 임의로 인증된 사용자를 추가한다. build.gradle에 관련 dependency를 추가하고

    testImplementation('org.springframework.security:spring-security-test')

PostsApiControllerTest의 메소드 상위에 인증된 모의 사용자를 추가합니다.

 // 책 기준 수정 메소드에도 추가해야함
    @Test
    @WithMockUser(roles = "USER")
    public void Posts_등록() throws Exception {

여기까지하면 될 거 같지만 우리가 추가한 @WithMockUser는 단순 @SpringBootTest에서는 작동하지 않고 MockMVC를 사용하도록 해야 작동하므로 코드를 추가합니다.

    @Autowired
    private WebApplicationContext context;

    private MockMvc mvc;
    
    @BeforeEach
    public void setup() {
        mvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity()) // SecurityMockMvcConfigurers에서 import(*server 안붙은거 주의)
                .build();
    }

기존 코드 수정

    @Test
    @WithMockUser(roles = "USER")
    public void Posts_등록() throws Exception {
    ...
        //when
        mvc.perform(post(url) // 1 기존 restTemplate 삭제 2 post는 MockMvcRequestBuilders에서 immport
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .content(new ObjectMapper().writeValueAsString(requestDto)))
               		    .andExpect(status().isOk());
        //then
        //위 두줄은 삭제
        
    @Test
    @WithMockUser(roles = "USER")
    public void Posts_수정된다() throws Exception {
    ...
        //when
        mvc.perform(put(url)
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .content(new ObjectMapper().writeValueAsString(requestDto)))
                        .andExpect(status().isOk());

        //then
        //위 두줄은 삭제

이 과정까지 마치면 정상적으로 테스트가 작동하지만 책에 소개되지 않은 내용 한 가지를 더 살펴볼 것이 있습니다. 이전 포스트에도 언급한 적 있지만 스프링부트 2.2부터 MediaType.APPLICATION_JSON_UTF8는 deprecated. 아마 인텔리제이 상에서 줄이 그어 있을 것이고 한글이 깨지는 현상이 해결되었을 것입니다. 이에 대한 자세한 설명을 하자면 스프링부트 2.2(스프링 5.2)부터는 json의 기본 charset이 utf8이 되었기에 위의 코드는 deprecated. 하지만 이번 수정 전 Test는 restTemplat을 사용했었고 request를 string으로 받았기에 한글이 깨져왔을 것입니다. 이를 모두 이해했으면 알겠지만 UTF8 부분을 지우는게 낫습니다.

 

3 @WebMvcTest 수정

- Hello테스트의 경우 1번과 비슷한 로그를 출력하지만 @WebMvcTest를 사용합니다. 따라서 설정을 만들기 위해 SecurityConfig는 읽었고 이를 생성해야하는데 @Controller를 제외한 @Repository 등의 @Component 계열 어노테이션은 스캔 대상이 아니기에 이런 에러가 납니다. 따라서 해당 테스트에 필터를 추가해 SecurityConfig를 스캔 대상에서 제외합니다.

@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = HelloController.class,
        excludeFilters = {
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = SecurityConfig.class)
        }
)
public class HelloControllerTest {

마찬가지로 임의 모의 사용자를 추가

    // Dto도 추가해야함
    @WithMockUser(roles = "USER")
    @Test
    public void hello가_리턴된다() throws Exception {

4 JPA Auditing 적용

- 이렇게 수정한 뒤 테스트해도 책 내용대로 아직 에러는 발생하지만 그 로그가 달라졌을 것입니다. 앞서 소개했듯 JPA는 데이터가 변경될 때마다 생성, 수정일자 같은 시간 칼럼이 입력되는데 이가 없으면 누가 언제 그 데이터를 변경했는지 알지 못하여 중복 등에 취약합니다. 이를 자동으로 해줬던 것이 JPA Auditing인데 이를 사용하려면 우리가 도메인에 작성했듯 시간에 대한 @Entity가 최소 하나는 존재해야하는데 @WebMvcTest엔 그게 없습니다. 이를 해결하기 위해 일단 스캔이 동시에 되지 않도록 @EnableJpaAuditing과 @SpringBootApplication을 분리합니다. 우선 main App에서 @EnableJpaAuditing 삭제 합니다.

// @EnableJpaAuditing 삭제
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

config 패키지에 JpaConfig 생성 후 @EnableJpaAuditing 추가

@Configuration
@EnableJpaAuditing
public class JpaConfig {
}

이로써 모든 테스트가 성공할 것입니다. 마지막으로 빌드 과정을 진행하며 이글을 보는 사람들은 로그 보고 대부분의 에러를 잡겠지만 혹시나 에러때문에 골머리를 썩고 있다면 다음의 사항을 참고하시길 바랍니다.

  • build.gradle에 dependency에 테스트 관련 라이브러리를 추가하지 않았거나 본인의 환경 버전을 체크할 것(gradle-gradle-wrapper.properties에서 gradle 버전 체크 가능)
  • 패키지 구조가 잘못됨(좌측 패키지부분 설정에 들어가 Tree Appearance를 변경해볼 것. 필자는 Compact Middle Package로 하여 보기 편함)
  • 필자의 포스트대로 진행했으면 yml 문법 오류(공백 하나때문에 작동 안할때 있음)거나 h2 서버를 켜지 않았을 수 있음
  • 이외에 늘 그렇듯 대소문자나 본인 파일 설치 경로에 한글이 있을때 문제가 발생할 수 있음

여기까지 마쳤다면 이 책의 전반부인 빌드 과정이 끝나고 후반부인 배포 과정이 시작됩니다. 빌드 과정에 있어 추후 커스텀이나 리팩토링은 필요하다면 추후 포스팅하겠습니다.

 

refer

이동욱님 블로그의 관련 포스트 : https://jojoldu.tistory.com/539?category=717427

개발바닥 유튜브 :https://www.youtube.com/channel/UCSEOUzkGNCT_29EU_vnBYjg

 

개발바닥

본격 세계최초 DEV 엔터테인먼트 토크쇼 두 스타트업 개발자의 요절복통 이야기 구독 안하면 장애남!!

www.youtube.com

이동욱님 github의 해당 repository : https://github.com/jojoldu/freelec-springboot2-webservice