본문 바로가기
프로젝트/토이 프로젝트

Spring + Redis + S3 + 이메일 인증 + Docker + CI/CD - 8

by 진꿈청 2024. 2. 28.

이번 포스팅에서는 AWS S3 테스트와 API 명세서를 작성한 과정을 담는다.

 

우선, AWS S3를 사용하기 위해 MockS3를 사용한다.

 

그 이유는 S3에 테스트를 위해 생성한 파일들이 계속 저장되기도 하고

프리티어 계정에는 GET/POST 요청이 횟수 제한이 있기 때문이다.

 

따라서, 관련된 설정을 해줘야 한다.

 

 

MockAwsS3Config

 

기존 AmazonS3 대신 MockAwsS3Config를 사용한다. 이때 사용되는 @Primary는 동일한 빈이 있을 때 우선순위를 설정해준다.

@Configuration
public class MockAwsS3Config extends AwsS3Config {

    @Bean
    @Primary
    @Override
    public AmazonS3 amazonS3Client() {
        return Mockito.mock(AmazonS3.class);
    }
}

 

 

UserIntegrationTest

 

이미지 저장/이미지 불러오기/이미지 삭제를 할 수 있는 테스트이다.

@Test
@DisplayName("이미지 저장")
public void saveProfileImageTest() throws Exception{
    //given
    User user = userService.findUserByEmail(EMAIL);

    //when
    MultipartFile multipartFile1 = new MockMultipartFile("file1", "file1.jpg", "image/jpeg", "file1 content".getBytes());
    String imageUrl1 = getImageUrl(multipartFile1);
    ImageDto imageDto = fileUploadService.save(user, multipartFile1);

    //then
    assertEquals(imageUrl1, imageDto.getImageUrl());

}

private String getImageUrl(MultipartFile multipartFile1) throws MalformedURLException {
    String imageUrl1 = "https://s3.ap-northeast-2.amazonaws.com/imagetest.file.bucket/image/example.png";
    when(amazonS3ResourceStorage.store(MultipartUtil.createPath(multipartFile1), multipartFile1)).thenReturn(new URL(imageUrl1));
    return imageUrl1;
}

@Test
@DisplayName("이미지 불러오기")
public void getProfileImageTest() throws Exception{
    //given
    User user = userService.findUserByEmail(EMAIL);
    CustomUserDetails userDetails = CustomUserDetails.of(user);

    TokenDto tokenDto = jwtTokenProvider.generateToken(userDetails);
    String accessToken = tokenDto.getAccessToken();
    String refreshToken = tokenDto.getRefreshToken();
    String encryptedToken = aes128Config.encryptAes(refreshToken);

    saveImage(user);

    //when
    String url = UriComponentsBuilder.newInstance().path(BASE_URL + "/images")
            .build().toUri().toString();

    ResultActions actions = ResultActionsUtils.getRequestWithToken(mockMvc, url, accessToken, encryptedToken);

    //then
    actions
            .andExpect(status().isOk())
            .andDo(document("get-profile-images",
                    getRequestPreProcessor(),
                    getResponsePreProcessor(),
                    UserResponseSnippet.getProfileSnippet()));
}

@Test
@DisplayName("이미지 삭제")
public void removeProfileImageTest() throws Exception{
    //given
    User user = userService.findUserByEmail(EMAIL);
    CustomUserDetails userDetails = CustomUserDetails.of(user);

    TokenDto tokenDto = jwtTokenProvider.generateToken(userDetails);
    String accessToken = tokenDto.getAccessToken();
    String refreshToken = tokenDto.getRefreshToken();
    String encryptedRefreshToken = aes128Config.encryptAes(refreshToken);

    String imageUrl = saveImage(user);

    //when
    String url = UriComponentsBuilder.newInstance().path(BASE_URL + "/image")
            .build().toUri().toString();

    ResultActions actions = ResultActionsUtils.deleteRequestWithTokenAndParam(mockMvc, url, accessToken, encryptedRefreshToken, imageUrl);

    //then
    actions
            .andExpect(status().isOk())
            .andDo(document("delete-profile-images",
                    getRequestPreProcessor(),
                    getResponsePreProcessor(),
                    UserResponseSnippet.deleteProfileImageSnippet()));
}


private String saveImage(User user) throws MalformedURLException {
    MultipartFile multipartFile1 = new MockMultipartFile("file1", "file1.jpg", "image/jpeg", "file1 content".getBytes());
    ImageDto save = fileUploadService.save(user, multipartFile1);
    return save.getImageUrl();
}


이렇게 해서 AWS S3 이미지를 테스트하였다.

 

 

 

그 후, OAuth2를 추가해서 로그인을 쉽게 하기위해 관련 내용을 살펴보았다.


옛날에 Spring Session 방식에서는 OAuth2를 구성해본적이 있으나 JWT를 활용한적이 없었다.

 

우선, JWT를 활용한 OAuth2 Logic은 다음과 같다.

  1. Resource Owner(일반 사용자)가 Client(SpringBoot Server)에 접근하기 위해 Resource Server(FrontEnd Server)의 로그인 창을 클릭한다.
  2. 로그인에 성공하면 Resource Server에 등록해놓은 Redirect URI(http://{Client IP 주소}/login/oauth2/code/{registrationId}?{code} 경로로 요청이 들어온다.
  3. Client는 {code} 값을 이용해서 다시 Resource Server로 Access Token을 요청한다.
  4. Client는 Access Token으로 다시 Resource Server로 scope에 해당하는 사용자 정보를 요청한다. (profile, email 등)
  5. 사용자 정보를 이용해 서비스의 회원인지 아닌지 판단하고 회원이라면 JWT로 Access Token과 Refresh Token을 만들어서 반환하고 웹사이트의 사용자가 아니라면 간단한 회원가입을 진행한다.

 

그림 설명은 아래와 같다.(참조)

 


우선, OAuth2를 사용하려면 아래와 같은 의존성을 추가해주자.

 

// Oauth2 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'


그러나, 하려고보니 아무리 봐도 이건 프론트엔드와 같이 작업을 해보는게 좋다고 생각을 했다.

기존에 해봤던 것이랑 비슷하기도 하고 포스트맨으로 동작 과정을 설명할텐데 애초에 하려던 작업이 아니여서 불필요하다는 생각이 들었다.

 

그리고 무엇보다 중요한건 내가 특정한 서비스를 목적으로 만든 것이 아니여서 받아오는 scope가 애매하다.

나이, 성별 등 무의미하게 사용하기가 좀 그렇기도 하고 원래 카카오, 네이버, 구글 등을 만들려했는데(액세스, 시크릿 키 생성 완료)

몇몇 클라이언트는 이메일을 비즈니스 사용자 아니면 제공을 안하는 것 같다.

 

따라서, Email을 Unique로 하여 사용자 식별에 사용하도록 설계한 나의 토이 프로젝트에 좀 적합하지 않다.

(물론, 임의의 값으로 이메일을 만들면 가능하긴 하다. OAuth2 Client가 전달해주는 식별 ID -> 식별ID@{email}.com 이런식으로 말이다.)

 

직접 코드도 구현을 했다만 다음 프로젝트에서 프론트와 같이 협업하여 구성해보려 한다.

 

 

 

그 후, API 명세를 쭉 작성했다.

 

Spring Rest Docs의 AsciiDoctor를 활용한

 

index.adoc

= API 문서
:toc: left
:toclevels: 2
:sectlinks:



== 서론

진꿈청의 API 문서에 오신 것을 환영합니다!

== 사용자 정보 API

유저 정보 HTTP Request

include::{snippets}/get-user/http-request.adoc[]

유저 정보 HTTP Response 예시
include::{snippets}/get-user/http-response.adoc[]

유저 정보 Request Body 예시
include::{snippets}/get-user/request-body.adoc[]

유저 정보 Response Body 예시
include::{snippets}/get-user/response-body.adoc[]

유저 정보 Response Field 예시
include::{snippets}/get-user/response-fields.adoc[]

---

업데이트 유저 HTTP Request

include::{snippets}/patch-user/http-request.adoc[]

업데이트 유저 HTTP Response 예시
include::{snippets}/patch-user/http-response.adoc[]

업데이트 유저 Request Body 예시
include::{snippets}/patch-user/request-body.adoc[]

업데이트 유저 Response Body 예시
include::{snippets}/patch-user/response-body.adoc[]

업데이트 유저 Response Field 예시
include::{snippets}/patch-user/response-fields.adoc[]

---

 


http://localhost:8080/docs/index.html 접속시

 

 

이런식으로 테스트에 성공한 API 명세서를 확인할 수 있다.

 

 

Postman

 

그리고 포스트맨까지 API 명세서 작업을 했다.

 

 

https://documenter.getpostman.com/view/24415208/2sA2rFRKMi#ee46b019-b7e1-4344-bbed-1bff32f630b7

 

SpringToy

# SpringToy API 명세서입니다. #### **Spring + Redis + S3 + 이메일 인증 + Docker + CI/CD 활용** #### **거의 모든 API는 토큰이 있어야 합니다.** ###### 자세한 개발 과정은 아래 주소로 참고할 수 있습니다. [https:/

documenter.getpostman.com

 

위 링크로 확인이 가능하다.


드디어 나의 좌충우돌 토이 프로젝트가 거의 끝났다.

 

 

마무리하며

나의 토이 프로젝트 목표는 최대한 내가 안해본 많은 기능, 구현들을 해보는 것이었다.

또한, 진행하는 중간에는 객체지향, 클린 코드 설계를 위해 어떻게 해야할지 고민하게 되었다.

그리고, Docker에 대한 관심과 CI/CD, Spring Cloud에 관한 관심이 생기게 되었다.

 

기존에 그냥 기록하지 않고 진행했던 나만의 프로젝트에서 이렇게 블로그 포스팅으로 남기니 더 기억이 잘 되는 것 같다.
내가 진행한 이 토이 프로젝트가 아직 코드적으로 많이 부족하지만 그래도 감은 잡은 것 같다.
또한, 관련하여 따로 스터디를 하며 클린코드, 객체지향적 설계를 공부할 예정이다.

 

깃허브 ReadMe 파일도 잘 정리해서 작성해야지!!!