코드스테이츠 백엔드 부트캠프 82~93일차 - PreProject 오류일기

wish17·2023년 4월 14일
0

오류정리

목록 보기
2/7
post-thumbnail

고생한 오류들

cors오류

결론부터 말하자면 내가 (Credentials)설정을 안하고 원인을 찾지 못해서 오래 고생했다.
아래는 내가 원인을 찾아가는 흐름이라고 생각하면 된다.

프론트측에서 cors 오류가 발생했다고 해결해 달라는 요청이 들어왔다.

이때 당시에는 RESTfull한 API를 디자인하기 위해서 "GET","POST", "PATCH", "DELETE"만 사용해야 한다고 잘못 알고 있었다.
(수업 과정에서 CRUD만 사용하고 이외의 방법을 배운적이 없었다...)

따라서 더 이상의 수정은 이루어질 수 없다고 확신해버렸다.

그러한 와중에 local에 대해 잘모르는듯한 모습을 보이셔서 이를 근거로 상대방이 틀렸다고 단정지어 버리고 내가 틀릴 수 있다는 가능성을 배제시켜 버렸다.

  • 상대방 local에서 동작하는 무언가에 내가 열어준 서버와 연결시키는 로직이 있을 수 있는데 이에 대한 가능성을 상대방에게 확인하지 않고 local에 대해서 모른다고 단정해버렸다.

  • 실제로 후에 알게 된 내용이 프론트분이 서버를 돌려서 접속하셨다...

    • 나는 얘기도 제대로 안해보고 당연히 3-Tier 아키텍쳐일거라 생각했다.
    • 3-Tier 아키텍쳐 맞다.. 클리이언트도 하나의 프로그램인 것이다. 프론트엔드 팀원분의 로컬이 내가 공유해준 배포 url과 연결되어 있던 것!

그런데 갑자기 요청이 정상적으로 들어오며 해결되었다.
이렇게 흐지부지 마무리 되었긴 하지만 후에 아래와 같이 추가로 연락이 왔다.


뭔가 gpt한테 한 질문을 봤을 때 백엔드에서 해결해줄 수 있는 문제였다고 생각하시는 것 같은데 보내주신 gpt의 답변에는 나에게 필요한 정보는 없었고 해결방법이 전혀 생각나지 않아서 찝찝한 상태로 끝나버렸다.

스스로를 조금 변호해 보자면...

  • 내 코드를 확인 후 문제가 없다고 판단되어 무엇을 수정해달라는건지 이해할 수 없어서 추가적인 정보로 request 헤더 등을 요구 했었지만 받지 못했다.
  • 고쳐주기 싫은게 아니라 고칠 수 있는게 없는 상태였던 것이다.
    • 따라서 대화를 통해 고쳐야하는 부분을 찾고자 했는데 판단을 위한 충분한 정보를 제공해주지 않으시고 질문만 하신다...(내가 고쳐줄 의사가 없다고 판단하시고 스스로 해결하려고 단념하신 것 같다.)

하지만 이대로 끝내기에는 너무나도 찝찝해서 답변을 안해주시니... 혼자 OPTIONS를 왜 사용하셨을지 찾아봤다.

찾아보니 프리플라이트 요청이라는 개념이 있었다.

프리플라이트 요청

  • 실제 요청을 보내기 전 OPTIONS로 미리 권한 확인을 해서 요청간의 낭비를 줄이는 방법

프론트분이 OPTIONS로 요청을 보낸 것이 실수나 잘못이 아니라 근거있는 행동이었던 것이다.
하지만 해당 방법을 사용하기 위해서는 서버에 OPTIONS 요청에 대한 처리가 있어야 한다.
그런데 당연히 이걸 처음본 나는 구현해두지 않았다.

프리플라이트 요청 방식을 사용하고자 했으면 통합회의에서 언급해 서로 조율했어야 했는데 이런 언급이 없었다는게 아쉽다고 느꼈다. 그래서 아래와 같이 솔직하게 말했다.

결국 서로 몰랐던 부분에 대해서 짚고 사과하며 몰랐던 부분을 배워갈 수 있는 경험이 되었다.

후에 ec2로 다시 배포하며 알게된 사실이 프리플라이트 요청방식을 사용해도 OPTIONS에 대한 cors만 열어주면 알아서 처리 된다....(OPTIONS에 대한 로직 구현 안해도 된다. 오히려 구현했다가 지웠다.)

cours 오류에 대해 정리하자면...

  • 나는

    • 내가 잘못알고 있을, 혹은 모르고 있을 가능성을 생각하지 않았다.(자만했다)
    • 정보도 불충분한 상황에 판단을 내리기 보다는 필요한 정보를 더 강하게 요구했어야 했다.
    • REST API의 정의에 대해 잘못알고 있었다.
  • 프론트분은

    • 요청을 할 때는 충분한 정보를 함께 준다면 더 좋을 것 같다.
    • 프리플라이트 요청 방식을 사용하려면 OPTIONS 요청을 허용해야하기 때문에 간략한 설명을 함께 해주셨으면 좋았을 것 같다.

나는 모든 출처를 허용해 둬서 cors가 서버 문제가 아닐거라 생각했지만 알고보니 동일출처가 아니면 브라우저에서 자동으로 프리플라이트 방식으로 OPTIONS 요청을 보낸다. 기본 CRUD만 구현해둬서 OPTIONS에 대한 요청을 제한해 뒀고 막혔던 것이였다.

이를 해결하기 위해서 서버에 OPTIONS 처리 로직을 추가하고, cors설정을 수정하면 될 것이라 예측했지만 해결되지 않았다.

결국 ngrok을 사용하지 않고 aws를 이용해 배포하기로 했다.
(ec2로 배포하면 아무런 문제도 발생하지 않음)

ec2로 배포하고도 문제가 발생했다.

로그인 과정에서 오류가 발생해서 설정 이것저것 바꾸다가...
Credentials을 로그인 과정에서는 필요 없다 생각해서 로그인 과정에서는
configuration.setAllowCredentials(Boolean.valueOf(false));와 같이 설정해 뒀는데 프론트측 코드에 Credentials = true로 설정해서 차단되었던 것이였다. (소통의 중요성...)

@Configuration
@EnableWebSecurity // Spring Security를 사용하기 위한 필수 설정들을 자동으로 등록
@EnableGlobalMethodSecurity(prePostEnabled = true) // 메소드 보안 기능 활성화
public class SecurityConfiguration {

	~~생략~~

    @Bean
    CorsConfigurationSource corsConfigurationSource() { // CorsConfigurationSource Bean 생성을 통해 구체적인 CORS 정책을 설정
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST", "PATCH", "DELETE"));  // 파라미터로 지정한 HTTP Method에 대한 HTTP 통신을 허용
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setExposedHeaders(Arrays.asList("*"));
        configuration.addAllowedHeader("*");
        configuration.setAllowCredentials(Boolean.valueOf(true));

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();   // CorsConfigurationSource 인터페이스의 구현 클래스인 UrlBasedCorsConfigurationSource 클래스의 객체를 생성
        source.registerCorsConfiguration("/**", configuration);      // 모든 URL에 앞에서 구성한 CORS 정책(CorsConfiguration)을 적용
        return source;
    }

	~~생략~~

}

위와 같이 수정한 뒤 ec2는 물론이고 아래와 같이 ngrok에서도 멀쩡히 실행되었다.


ec2 서버 터짐

ctrl + c 를 입력해도 명령 종료가 안되고 자꾸 터졌었다.
배포 처음 배울 때도 이 문제로 상당히 시간을 많이 뺏겼었는데 의심가는 원인을 찾았다.

ps -ef | grep java 명령어를 통해 확인시 아래와 같이 백그라운드에서 애플리케이션이 실행중이다.

백그라운드 실행중인걸 까먹고 동일 포트에서 작업을 명령해서 문제인 것 같다.
백그라운드에서 돌고 있을 수 있다는건 당연히 알지만... 여러 작업을 하며 순간순간 까먹었던 것 같다.

해당 문제를 인지한 뒤 매번 백그라운드 체크하고 kill -9 [PID] 명령어를 이용해 확실히 종료하고 실행하니 더 이상 서버가 터진적이 없다.

까먹지 말고 빌드나 실행전에는 수시로 백그라운드 체크를 해야겠다.


가벼운 오류들

생성한 클래스 참조를 위한 import 안되는 오류

분명히 생성한 클래스의 접근제어자도 public이고 오타로 인한 문제도 아니며, 미완성된 클래스도 아닌데 자동 import가 안되길래 직접 import문을 하드코딩해 작성해도 import가 안되는 오류가 발생했다.

원인: 클래스 생성 과정에서 오타로 인해 클래스명을 수정했었다.
(쿠키 문제)

해결법: 인텔리제이 재시작하니 해결


Authentication 객체 호출 시 NullPointerException

  • 변수명 최대한 바꾸지 말자...
  • 불가피하게 바꿔야하면 열심히 확인해야 한다...
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

	~~생략~~

    // Access Token을 생성하는 구체적인 로직
    private String delegateAccessToken(Member member) {
        Map<String, Object> claims = new HashMap<>();
----------------------------------수정한 부분----------------------------------        
        claims.put("memberEmail", member.getEmail());
----------------------------------수정한 부분----------------------------------        
        claims.put("roles", member.getRoles());
        claims.put("memberNickName", member.getMemberNickName());

        String subject = member.getEmail();
        Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes());

        String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());

        String accessToken = jwtTokenizer.generateAccessToken(claims, subject, expiration, base64EncodedSecretKey);

        return accessToken;
    }
    
    ~~생략~~
    
}

claims에 담기는 email 변수명이 직관적이지 않아 변경했는데 아래 JWT토큰 검증 과정에서 호출되는 부분을 수정하지 않아 오류가 발생했었다.

// JWT 검증 필터 구현 클래스
// 클라이언트 측에서 전송된 request header에 포함된 JWT에 대해 검증 작업을 수행하는 코드
public class JwtVerificationFilter extends OncePerRequestFilter {  // OncePerRequestFilter 상속받아서 request 당 단 한 번만 수행

    private void setAuthenticationToContext(Map<String, Object> claims) { //  Authentication 객체를 SecurityContext에 저장하기 위한 메서드
----------------------------------수정한 부분----------------------------------    
        String username = (String) claims.get("memberEmail");   // 파싱한 Claims에서 username 얻기
----------------------------------수정한 부분----------------------------------        
        List<GrantedAuthority> authorities = authorityUtils.createAuthorities((List)claims.get("roles"));  //  Claims에서 권한 정보 얻기
        Authentication authentication = new UsernamePasswordAuthenticationToken(username, null, authorities);  // 이미 앞에서 인증된 Authentication객체를 생성하는데 비밀번호는 불필요하니까 null입력
        SecurityContextHolder.getContext().setAuthentication(authentication); //  SecurityContext에 Authentication 객체를 저장
    }

}

메서드의 변경은 인텔리제이에서 지원하는 사용 위치 기능을 통해 바로 보여서 실수하기 힘들지만 이렇게 변수명 등의 변경은 눈으로 찾기 힘들고 한번에 모든 파일을 검색할 방법이 없으니 최대한 바꾸지 않도록 해야할 것 같고 불가피하게 바꿔야하면 키워드 탐색기능을 통해 연관된 클래스를 전부 체크할 수 있도록 해야겠다.


jsonPath 데이터 경로 찾는방법

jsonPath(".data.email")jsonPath(".data.email")와jsonPath(".email")의 차이점에 대해 구분하지 못해 노가다로 찾아 냈다...

첫번째 yml 설정

spring:
  h2:
    console:
      enabled: true
      path: /h2
  datasource:
    url: jdbc:h2:mem:test
  jpa:
    hibernate:
      ddl-auto: create  # (1) 스키마 자동 생성
    show-sql: true      # (2) SQL 쿼리 출력
    properties:
      hibernate:
        format_sql: true  # (3) SQL pretty print
  sql:
    init:
      data-locations: classpath*:db/h2/data.sql
logging:
  level:
    org:
      springframework:
        orm:
          jpa: DEBUG
server:
  servlet:
    encoding:
      force-response: true

두번째 yml 설정

logging:
  level:
    org:
      springframework:
        web: debug
    org.springframework.web.servlet: debug
    org.hibernate.type.descriptor.sql.BasicBinder: trace

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/pre033?serverTimezone=Asia/Seoul
    username: root
    password: ${localDBPassword}
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
    log-request-details: true
  jpa:
    defer-datasource-initialization: true
    hibernate.ddl-auto: create-drop
    open-in-view: false
    show-sql: true
    properties:
      hibernate.format_sql: true
      hibernate.default_batch_fetch_size: 100
#  sql.init.mode: always
mail:
  address:
    admin: admin@gmail.com
jwt:
  key: ${JWT_SECRET_KEY}
  access-token-expiration-minutes: 30
  refresh-token-expiration-minutes: 420

server:
  port: 8080

두가지 설정이 데이터를 저장하는 위치가 다른 것은 인지하고 있지만 아래 test코드에서 사용하는 데이터는 메모리에 저장하는데 불러오는 경로를 다르게 작성해야 된다. 그런데 왜 그런지 모르겠다.
(아니... 메모리에 저장하는건데 yml 설정이랑 관련 없지 않나? ㅠㅡㅠ)

메모리에 저장하는거니 yml 상관 없는거 맞다.

// 첫번째 yml 설정의 경우
@Test
void getMemberTest() throws Exception {
    MemberDto.Response response = makeMemberResponse();

    given(memberService.findMember(Mockito.anyLong()))
            .willReturn(new Member());
    given(mapper.memberToMemberResponse(Mockito.any(Member.class)))
            .willReturn(response);


    mockMvc.perform(
            get("/v11/members/{member-id}", response.getMemberId())
                    .accept(MediaType.APPLICATION_JSON)
                    .contentType(MediaType.APPLICATION_JSON)
    ).andExpectAll(
            status().isOk(),
-----------------------------------다른부분-----------------------------------           
            jsonPath("$.data.name").value(response.getName()),
            jsonPath("$.data.phone").value(response.getPhone())
-----------------------------------다른부분-----------------------------------            
    )
}


// 두번째 yml 설정의 경우
@Test
void getMemberTest() throws Exception {
    MemberJoinResponseDto response = makeMemberResponse(1L);

    given(memberService.findMember(Mockito.anyLong()))
            .willReturn(new Member());
    given(mapper.memberToMemberResponse(Mockito.any(Member.class)))
            .willReturn(response);


    mockMvc.perform(
            get("/members/{member-id}", response.getMemberId())
                    .accept(MediaType.APPLICATION_JSON)
                    .contentType(MediaType.APPLICATION_JSON)
    ).andExpectAll(
            status().isOk(),
-----------------------------------다른부분-----------------------------------            
            jsonPath("$.memberId").value(response.getMemberId()),
            jsonPath("$.email").value(response.getEmail()),
            jsonPath("$.profileImage").value(response.getProfileImage()),
            jsonPath("$.memberNickName").value(response.getMemberNickName())
-----------------------------------다른부분-----------------------------------
    )
}

각각의 경우 위와 같이 경로를 다르게 지정해야만 정상적으로 작동한다.
하지만 아래의 list 데이터를 다루는 경우는 경로가 똑같다.

//두번째 yml 설정
@Test
void getMembersTest() throws Exception {
    Member member1 = makeMember(1L);
    Member member2 = makeMember(2L);

    MemberJoinResponseDto response = makeMemberResponse(1L);
    MemberJoinResponseDto response2 = makeMemberResponse(2L);

    int page = 1;
    int size = 10;

    Page<Member> pageList = new PageImpl<>(List.of(member1,member2), PageRequest.of(page-1,size, Sort.by("memberId").descending()),2);
    List<MemberJoinResponseDto> memberList = new ArrayList<>();
    memberList.add(response);
    memberList.add(response2);

    given(memberService.findMembers(Mockito.anyInt(),Mockito.anyInt()))
            .willReturn(pageList);
    given(mapper.membersToMemberResponses(Mockito.anyList()))
            .willReturn(memberList);

    mockMvc.perform(
            get("/members?page="+page+"&size="+size)
                    .accept(MediaType.APPLICATION_JSON)
    ).andExpectAll(
            status().isOk(),
            jsonPath("$.data[0].email").value(member1.getEmail()), // 페이지네이션 정렬에 따른 순서 주의
            jsonPath("$.data[1].email").value(member2.getEmail())
    )

이게 대체 왜 그럴까?

  • 일단 실행 로그를 통해서 멤버 전체 조회의 경우 둘 다 data에 묶여서 결과값이 나오는 것을 알 수 있었다.(고로 경로 똑같이 $.data[0].email로 작성하면 된다.

  • 단건 조회로 테스트해 보면 아래와 같이 나온다.

    • 첫번째 설정 : Body = {"data":{"memberId":1,"email":"hgd@gmail.com","name":"홍길동","phone":"010-1111-1111","memberStatus":"활동중","stamp":0}}
    • 두번째 설정 :{"memberId":1,"email":"test1@gmail.com","profileImage":"https://avatars.githubusercontent.com/u/120456261?v=4","memberNickName":"NickName1"}

로그를 보고 경로를 유추할 수 있긴 하게 됐지만 어떤 설정의 차이로 인해서 data로 값이 묶이고 안묶이는지 구분을 못하겠다.

answer: responsedto의 설정 차이였다.

고로 멍청한 의문이였다 ㅠㅡㅠ 내가 묶어두고서는 까먹었다...


순환참조오류

2023-04-21 06:11:09.955 ERROR 18056 --- [  restartedMain] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  securityConfiguration defined in file [D:\AAWonJong\it\seb43_pre_033\digitalWizard-server\build\classes\java\main\com\seb33\digitalWizardserver\config\SecurityConfiguration.class]
↑     ↓
|  memberService defined in file [D:\AAWonJong\it\seb43_pre_033\digitalWizard-server\build\classes\java\main\com\seb33\digitalWizardserver\member\service\MemberService.class]
└─────┘


Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.


종료 코드 0()로 완료된 프로세스

Oauth2 구글 로그인 인증 기능을 추가하다 순환 참조에 빠져버렸다.

어쩔 수 없이 아래와 같이 memberService를 사용하지 않고 직접 저장하도록 수정했다.

    private Member saveMember(String email, String nickname, String profileImage) {
        memberRepository.findByEmail(email).ifPresent(it ->
        {throw new BusinessLogicException(ExceptionCode.MEMBER_EXISTS, String.format("%s is duplicated 버그발생! OAuth2 핸들러 검사하시오.", email));
        });
        Member member = new Member();
        member.setEmail(email);
        member.setMemberNickName(nickname);
        member.setProfileImage(profileImage);
        Member savedMember = memberRepository.save(member);
        List<String> roles = authorityUtils.createRoles(email);
        savedMember.setRoles(roles);
        CustomMemberDto.from(savedMember);

        return member;
    }

이 방법은 확장성 측면에서도 좋지 않고 코드의 중복이 발생해 좋지 않은 방법이라고 생각한다.

하지만 최종 배포까지 생각하면 개발 가능한 기간이 2일밖에 남지 않아 매개 객체를 이용하면 다른 팀원의 코드까지 수정해야하는 상황이 발생할 수 있기 때문에 차선책으로 선택했다.


OAuth2 redirect_uri_mismatch

redirect_uri_mismatch라고 안내문이 나와서 OAuth2MemberSuccessHandler에서 리다이렉트하는 URI가 잘못되었다는줄 알고 조금 고생했다.

아래 코드를 계속 바꿔도 안되길래 이게 뭔가 했다...

    private URI createURI(String accessToken, String refreshToken) {
        MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
        queryParams.add("access_token", accessToken);
        queryParams.add("refresh_token", refreshToken);

        return UriComponentsBuilder
                .newInstance()
                .scheme("http")
                .host("seb43-pre-033.s3-website.ap-northeast-2.amazonaws.com") // Todo 리액트 서버 도메인 주소 입력
//                .port(3000) // Todo 포트번호 변경 주의
                .queryParams(queryParams)
                .build()
                .toUri();
    }

구글 Cloud 설정에서 서버 배포 주소를 허용해주니 해결되었다.
(적용하고 5분~몇시간 걸린다고 나와있는데 진짜 몇시간 걸려서 안되는건줄 알았다...)

0개의 댓글