4 트러블슈팅 - 97kim/mytrip GitHub Wiki

3차 프로젝트

👾1

로그인 시 userId를 클라이언트에게 넘겨주고 로컬 스토리지에 저장했습니다. 로그인한 사용자의 userId가 필요한 기능에 매번 로컬 스토리지에서 userId를 가져와 서버로 보냈었습니다.

이게 과연 효율적일까라는 생각에 로그인한 사용자의 userId를 서버에서 사용할 수 있는 방법을 인터넷에 찾아보았습니다.

  1. SecurityContextHolder를 통해 가져오기
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 
UserDetails userDetails = (UserDetails) principal; 
String username = userDetails.getUsername(); 
String password = userDetails.getPassword();
  1. Principal 객체에 접근해 정보를 가져오기
@RestController 
public class SecurityController { 
    @GetMapping("/username") 
    public String currentUserName(Principal principal) { 
        return principal.getName(); 
    } 
}
  1. Authentication 사용
@RestController
public class SecurityController {
    @GetMapping("/username")
    public String currentUserName(Authentication authentication) {
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        return userDetails.getUsername();
    }
}
  1. @AuthenticationPrincipal을 이용하여 현재 로그인한 사용자 객체를 인자에 주입하기
자세한 내용은 아래 링크에

출처: https://itstory.tk/entry/Spring-Security-현재-로그인한-사용자-정보-가져오기 [덕's IT Story]


구현했던 기능들에 쉽게 @AuthenticationPrincipal를 붙이면 될 것 같아 이 방법을 사용하기로 했습니다.

따라서 서버에서 @AuthenticationPrincipal를 사용해 로그인한 사용자 정보를 가져와 게시글 작성자 정보와 비교해서 기능이 작동하게 했습니다.

public class UserDetailsImpl implements UserDetails {

    private final User user;

    public UserDetailsImpl(User user) {
        this.user = user;
    }

    public User getUser() {
        return user;
    }

    public Long getId() {
        return user.getId();
    }

    ...
}
@RequiredArgsConstructor
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("예외 문구"));

        return new UserDetailsImpl(user);
    }
}
@DeleteMapping("/reviews/{reviewId}")
public ResponseEntity<?> deleteUserReview(@PathVariable Long reviewId,
                                          @AuthenticationPrincipal UserDetailsImpl nowUser) {
    return userReviewService.deleteUserReview(reviewId, nowUser);
}

👾2

스프링 시큐리티 설정에서 특정 uri에 권한을 지정할 수 있는데 HTTP 메소드별로 권한을 다르게 줄 수 있다는 것을 알게 되었습니다.

http.authorizeRequests()
    .antMatchers(HttpMethod.POST, "/userReview/comment/**").authenticated()

👾3

B 테이블이 A 테이블을 참조한다 했을 때 아무 설정 없이 A 테이블의 ROW를 삭제하려고 했을 때 참조관계에 문제가 생겼습니다. 매핑 시 Cascade.REMOVE를 넣어 A ROW를 삭제했을 때 이를 참조하는 B ROW도 삭제되게 했습니다.


👾4

CORS 설정 시 IllegalArgumentException 발생

@Bean
public CorsConfigurationSource corsConfigurationSource(){

    CorsConfiguration configuration = new CorsConfiguration();

    configuration.addAllowedOrigin("*");
    configuration.setAllowedMethods(Arrays.asList("GET","POST","PUT","PATCH","OPTIONS","DELETE"));
    configuration.setAllowedHeaders(Arrays.asList("*"));
    configuration.setAllowCredentials(true);
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);

    return source;
}

위 코드를 사용했을 때 아래와 같은 이슈가 있었습니다.

[IllegalArgumentException] When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.

configuration.addAllowedOrigin("*");처럼
'Access-Control-Allow-Origin'이 와일드카드(*)로 설정된 경우에는 자격증명(credentials flag)을 사용할 수 없습니다.

configuration.addAllowedOrigin("https://www.kimkj.shop");와 같이 지정해주거나 configuration.addAllowedOriginPattern("*");을 사용하면 해결됩니다.


👾5

용량이 큰 파일(이미지) 업로드 시 발생한 문제입니다.

org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException: The field review_img exceeds its maximum permitted size of 1048576 bytes.

업로드할 수 있는 최대 용량을 넘긴 파일을 업로드할 때 발생하는 에러입니다. 1048576 bytes는 약 1MB로 용량 설정을 하지 않았을 때의 디폴트 값입니다.

application.properties 파일에

spring.servlet.multipart.maxFileSize=50MB
spring.servlet.multipart.maxRequestSize=50MB

추가하면 됩니다. 위의 50MB처럼 최대 파일 용량을 조정할 수도 있습니다.

참고: https://artiiicy.tistory.com/8


👾6

com.amazonaws.services.s3.model.AmazonS3Exception:` Access Denied (Service: Amazon S3; Status Code: 403; Error Code: AccessDenied;

리뷰 삭제 시 S3에 올라간 이미지 파일도 삭제되도록 했습니다. 어느 순간부터 삭제가 되지 않았는데 확인해보니 aws 사용자 key가 깃허브에 노출 됐던 이슈가 있었습니다. 그래서 노출되었던 리포지토리를 삭제시켰습니다. 이후에 해당 사용자 key도 삭제시키고 다른 사용자를 생성해 key를 발급받았습니다. 보안에 신경쓰도록 합시다!
혹시 모를 상황을 대비해 aws 비밀번호도 변경하고 MFA도 적용했습니다.


👾7

프로젝트를 도커 이미지로 만들어 ECR로 Push 하고 도커 환경의 AWS EB에 자동 배포시키는 과정을 구축하려고 하던 중에 발생했습니다.

Dockerfile을 아래처럼 작성하고 GitHub Actions를 실행했습니다.

FROM openjdk:8-jdk-alpine
COPY build/libs/*.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

하지만 아래와 같은 오류가 발생했습니다.

When using COPY with more than one source file, the destination must be a directory and end with a /

에러 로그를 확인해보면 COPY 하려는 파일이 한 개가 아니다는 내용입니다.

스크린샷 2021-12-06 오후 8 45 01

로컬에서 확인해본 결과 build/libs에 *.jar 형태인 파일이 2개가 있었습니다. plain.jar도 생성이 되는데 이를 사용하지 않습니다.

plain.jar는 어플리케이션 실행에 필요한 모든 의존성을 포함하지 않고, 작성된 소스코드의 클래스 파일과 리소스 파일만 포함합니다.
(참고: https://dev-j.tistory.com/22)

따라서 build.gradle에 아래 코드를 추가해 자동으로 생성되지 않도록 했습니다.

jar {
    enabled = false
}

👾8

좋아요와 즐겨찾기 기능에서 uncheck를 하게 되면 다른 사용자들이 check 했던 데이터들도 DB에서 같이 삭제되는 문제가 있었습니다. 삭제 로직에서 게시물의 id값만을 변수로 주었기 때문에 구분 없이 삭제된 것이었습니다. 따라서 구체적으로 체크를 해제할 게시물을 특정지을 수 있도록 접속한 유저의 id값을 추가하고 조회용 함수에도 연결키워드를 통해 연결해 주어 해결했습니다.


👾9

AWS EB에 AWS ECR 이미지를 업로드하는데 이슈들이 계속 있었습니다.
헬스체크도 계속 빨간색으로 뜨며 제대로 배포가 되지 않았습니다. 여러 번 시도해보니 무엇이 문제였는지 알 수 있었습니다.

AWS EB의 역할에 권한을 주지 않아서 이슈가 발생했습니다. AWS EB - 구성 - 보안에 있는 EB의 역할을 찾아 AmazonEC2ContainerRegistry에 접근할 수 있는 권한을 추가했습니다.


👾10

H2 데이터베이스로 테스트를 했을 때는 데이터베이스에 이모지가 잘 들어갔습니다. 배포 후 리뷰를 작성할 때 이모티콘을 넣어보려 했지만 잘 작동하지 않았습니다. 인터넷을 찾아보니 charset의 문제가 있었습니다. 단순한 한글 표시까지는 utf8만 써도 되지만, 요즘 많이 사용되는 기본 이모지 등은 utf8mb4를 사용해야 표현이 가능합니다. RDS 콘솔(mySQL 기반)에 들어가 charset 항목들을 utf8mb4로 적용시켜주었습니다.


👾11

Swagger로 API 문서 자동화를 진행했습니다. 실행 후 Request URL을 보니 http로 요청을 보내는 것을 확인할 수 있었습니다.
Application.java 파일에 @OpenAPIDefinition(servers = {@Server(url = "/", description = "api.kimkj.shop")})를 추가하면 https로 요청을 보냅니다.

@OpenAPIDefinition(servers = {@Server(url = "/", description = "api.kimkj.shop")})
@SpringBootApplication
public class MytripApplication {
...
}
스크린샷 2021-12-10 오전 3 43 50

(참고: https://github.com/springdoc/springdoc-openapi/issues/726)


👾12

시간을 저장하기 위해서 LocalDateTime을 사용했습니다. Java 8 이후부터는 이 클래스를 사용해 로컬의 시간을 가져올 수 있습니다.
RDS 파라미터 그룹에서 timezone을 Asia/Seoul로 변경했습니다. RDS에는 시간이 한국 시간으로 잘 저장되었습니다.
하지만 배포 후 저희 사이트에서 작성 및 수정 시간, 댓글 시간이 한국 시간보다 9시간이 느리게 나왔습니다.

1. 첫 번째 시도 방법
EB 로그에서 timestamp를 확인하면 UTC로 되어 있어서 한국 시간으로 변경했습니다. EB를 사용하고 EC2 인스턴스의 timezone을 변경하려면 아래와 같이 하면 됩니다.
.ebextensions/00-set-timezone.config 파일을 프로젝트 최상단에 추가 후, 아래 코드를 입력하고 배포를 하면 EC2의 시간이 한국 시간으로 변경됩니다.

commands:
  set_time_zone:
    command: ln -f -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime

EB 로그는 한국 시간으로 뜨지만 저희 사이트에서 확인되는 시간은 똑같이 9시간이 느리게 나왔습니다.

2. 두 번째 시도 방법
MytripApplication.java에 아래 코드를 추가해 스프링 서버의 timezone을 한국 시간으로 변경했습니다.

@PostConstruct
public void setTimeZone() {
    TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
}

스프링 서버 timezone을 변경하니 한국 시간으로 잘 나왔습니다. 직렬화가 잘 됐는지 Postman으로 테스트했는데 한국 시간으로 잘 나왔습니다.

이 방법 외에 또 다른 방법을 찾았습니다.

3. 세 번째 시도 방법
Dockerfile에

RUN ln -snf /usr/share/zoneinfo/Asia/Seoul /etc/localtime && echo Asia/Seoul > /etc/timezone

위 코드를 추가했습니다. 생각해보니 저희 아키텍처상 스프링 프로젝트가 도커 이미지로 빌드되어 도커 컨테이너에서 실행되기 때문에 도커 timezone을 변경해야 했습니다. Postman으로 테스트했을 때도 한국 시간으로 잘 나온 것을 확인했습니다.


👾13

input 창에 script문 넣었을 때 적용되던 이슈를 해결한 과정은 아래 블로그에 적어두었습니다.

https://velog.io/@rudwnd33/xss-test


👾14

1MB가 넘는 이미지 파일을 업로드 했을 때 413 에러 + CORS 에러 뜬 문제가 있었는데 해결 방법을 아래 블로그에 적어두었습니다.

https://velog.io/@rudwnd33/413-error-cors

⚠️ **GitHub.com Fallback** ⚠️