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

wish17·2023년 4월 10일
0
post-thumbnail

자세히는 깃헙에서...

Github BE팀 브랜치 주소

  • 특별한 이슈가 발생하지 않는한 기록보다는 기능구현에 더 집중하고자 한다. 자세한 코드구현 등의 기록들은 어차피 github에서 볼 수 있으니 생략한다.

작업일기

04/12(수)

pre-project를 함께하게 된 팀원들과의 첫만남

아직 무엇을 만들지에 대해서도 모르기 때문에 간력하게 회의를 진행할 때 사용할 툴(discord, google meet)을 정하는 등 가벼운 인사를 나누고 백엔드 기능을 담당하는 팀(이하 BE팀)은 따로 남아 아래 회의록과 같은 내용에 대해서 이야기를 나눠봤다.

회의록

1. 스키마
2. api문서화(+테스팅)
    - TDD → RestDocs → 코드양이 너무 많고 오래걸림 ← 2단계
    - Swagger3.0 → 편하긴 하지만 실서비스 코드에 영향이 있음
    - Postman docs → ?.?.?.? ← 1단계
3. 컨벤션 + 커밋메세지 + 커밋약속(실행되는것만 커밋 등)
4. 다른사람 패키지 최대한 건들지 않기
    - if 건들게 되면 꼭 연락 먼저하기
5. 역할분담
    1. 시큐리티(세션+쿠키, Oauth2) + 기능 - 원종
    2. 배포+ 기능 - 현민
    3. db + 기능 - 석현 (postgreSQL)
6. 함께
    1. 테스팅 + API문서화 각자 맡는 파트
    2. member 같이
    3. 스키마 같이
7. 학습할 내용
    - Postman docs 문서화 뽑기
8. ****통합개발환경(Integrated Development Environment, IDE)****
    - 인텔리제이
9. ****JDK(Java Development Kit)****
    - JDK11
10. ****프레임워크(Framework)****
    - Spring-boot 2.7.1 버전
11. ****빌드 방식****
    - Gradle
12. DB
    - postgreSQL

04/13(목)

사용자 요구사항 정의서

간단하게 사용자 요구사항 정의서를 만들어 봤다.



회원가입 인증 방식 추가에 따른 고민

OAuth2 인증 방식을 추가할 때 회원 관리를 위해 아래와 같은 2가지 case 중 어떤 방식을 선택할지 정해야 했다.

1. 데이터 테이블을 하나 추가하는 방식

장점:

  1. OAuth2 인증과 기존 인증 방식을 명확하게 구분할 수 있다.
  2. 각 인증 방식에 따른 데이터 구조를 따로 관리할 수 있어 유지보수가 용이하다.
  3. OAuth2 인증 방식을 사용하는 회원이 증가할 경우, 성능상 이점이 있을 수 있다.

단점:

  1. 회원 관리 로직이 더 복잡해질 수 있다.
  2. 중복 데이터가 발생할 수 있으며, 이로 인해 데이터 정합성 문제가 발생할 수 있다.
  3. 데이터 테이블 간 관계 설정이 필요할 수 있다.

2. 기존 인증방식에 필요한 데이터 중 부족한 부분을 더미 데이터로 채우는 방식

장점:

  1. 회원 데이터를 한 테이블에서 관리하므로 구현 및 유지보수가 간단합니다.
  2. 회원 관리 로직이 단순해진다.

단점:

  1. 더미 데이터로 인해 데이터 정합성 문제가 발생할 수 있다.
  2. OAuth2 인증 방식과 기존 인증 방식의 차이를 처리하기 위한 추가 로직이 필요하다.

결론적으로, 회원 관리를 위해 데이터 테이블을 추가하는 방식은 명확한 구분과 데이터 구조 관리의 이점이 있지만, 구현 및 유지보수의 복잡성이 높아질 수 있습니다. 반면, 기존 인증방식에 필요한 데이터 중 부족한 부분을 더미 데이터로 채우는 방식은 구현이 간단하나, 데이터 정합성 문제가 발생할 수 있다.

위와 같은 장단점을 고려해 BE팀에서 의논을 통해 데이터 테이블을 하나 추가하는 방식으로 진행하기로 결정했다.

테이블 설계 (객체 관점)

위와 같은 자잘한 협의과정을 통해 약속을 정하고 테이블 설계에 들어갔다.

학습과정에서 테이블관점의 스키마를 배워 다른 BE팀원들이 객체 관점의 스키마 구조에 익숙하지 않았지만, 각 entity의 연관관계를 명확히 하고 내용을 공유하기 쉽게하기 위해서 객체 관점으로 스키마를 그리자고 설득하고 약속했다.

// Use DBML to define your database structure
// Docs: https://www.dbml.org/docs

Table Answer {
  answer_id integer
  member Member
  post Post
  body varchar [note: 'Content of the Answer']
  answer_comment List(Answer_Comment)
  vote Vote
  created_at LocalDateTime
  modifiedAt LocalDateTime
}

Table Answer_Comment {
  comment_id integer
  answer Answer
  member Member
  body varchar [note: 'Content of the Comment']
  created_at LocalDateTime
  modifiedAt LocalDateTime
}

Table Member {
  member_id integer [primary key]
  member_name varchar
  member_nick_name varchar
  email varchar
  profile_image varchar
  password varchar
  role varchar

  post List(post)
  answer List(answer)
  answer_comment List(answer_comment)
  save_post_id List(post_id)
  save_answer_id List(answer_id)
  notification_list list(Notification)

  location varchar [note: 'default = null']
  title varchar [note: 'default = null']
  aboutMe varchar [note: 'default = null']

  modifiedAt LocalDateTime
  created_at LocalDateTime
}

Table Member_Oauth2 {
  member_oauth2_id integer [primary key]
  member_oauth2_name varchar
  member_oauth2_nick_name varchar
  oauth2_email varchar
  profile_image varchar
  role varchar

  post List(post)
  answer List(answer)
  answer_comment List(answer_comment)
  save_post_id List(post_id)
  save_answer_id List(answer_id)
  notification_list list(Notification)

  location varchar [note: 'default = null']
  title varchar [note: 'default = null']
  aboutMe varchar [note: 'default = null']

  modifiedAt LocalDateTime
  created_at LocalDateTime
}

Table Post {
  post_id integer [primary key]
  title varchar
  body varchar [note: 'Content of the post']
  user_id integer
  status varchar
  answer List(Answer)
  post_like Post_Like
  post_hate Post_Hate
  created_at LocalDateTime
  modifiedAt LocalDateTime
  member Member
  view View
  article_hashTag_list list(article_hashTag)
}

Table HashTag {
  hashTag_id integer [primary key]
  hashTag_name varchar
  article_hashTag_list list(article_hashTag)
}

Table Article_HashTag {
  article_hashTag_id integer [primary key]
  hashTag HashTag
  post Post
}

Table Notification {
  notification_id integer [primary key]
  member Member
  notification_type notification_type
}

Table View {
  view_id integer [primary key]
  post Post
}

Table Post_Like {
  like_id integer [primary key]
  statusPlus boolean
  post Post
}

Table Post_Hate {
  hate_id integer [primary key]
  status boolean
  post Post
}

Table Vote {
  vote_id integer [primary key]
  isvoted boolean
  answer Answer
  modifiedAt LocalDateTime
}



Ref: "Member"."post" < "Post"."member"

Ref: "Member"."save_post_id" < "Post"."post_id"





Ref: "Answer_Comment"."answer" < "Answer"."answer_comment"

Ref: "Member"."save_answer_id" < "Answer"."answer_id"

Ref: "Member"."answer" < "Answer"."member"

Ref: "Member"."answer_comment" < "Answer_Comment"."member"

Ref: "Post"."answer" < "Answer"."post"

Ref: "Post"."view" < "View"."post"

Ref: "Post"."post_like" < "Post_Like"."post"

Ref: "Post"."post_hate" < "Post_Hate"."post"

Ref: "Post"."article_hashTag_list" < "Article_HashTag"."post"

Ref: "HashTag"."article_hashTag_list" < "Article_HashTag"."hashTag"

Ref: "Member"."notification_list" < "Notification"."member"

Ref: "Answer"."vote" - "Vote"."answer"


04/14(금)

요구사항 정의서

아래 첨부한 캡쳐본과 같이 04/13날 BE팀이 작성한 요구사항 정의서에 대해서 FE팀이 질문사항을 정리해와 회의를 진행하며 서로의 의견을 조율하며 중요도나 response request에 담아줘야할 내용들에 대해 정리했다.

api 명세서

  • FE팀과 BE팀이 함께하는 회의(이하 통합회의)를 통해 작성했던 api명세서에 대해 FE팀에게 설명해 줬다.
    • FE팀의 피드백이나 수정 요청은 없었다.

스키마

  • 게시물을 post 라고 명명해 소통 과정에서 post메서드와 혼동하는 경우가 생겨 question으로 변경했다.

작업현황

BE팀은 본격적으로 이슈를 등록하고 작업을 시작하려고 한다.

BE팀 총원 3명이 함께 기본구조가 될 Member, JWT, Exception을 작성하며 작명 규칙을 통일하는 등 전반적인 구조에 대한 설계방향에 대해 약속했던 내용들을 확인할 수 있었다.


04/15(토)

feat : 인증오류 처리

docs : ExceptionCode(HTTPStatus -> int)

httpstatus 타입으로 적는 것 보다 int타입으로 적는게 더 일반적이라고 들어서 수정했다.

feat : 인증오류 처리

인증오류시 클라이언트에 응답 보내주고 log 남기는 로직을 추가했다.


04/16(일)

  • member 기능구현을 하며 회원가입 완료 시 이메일을 보내보면 좋을 것 같다는 생각이 들었다. (후에 기능추가 하도록 이슈로 기록해뒀다.)

    • 사용자 메일 전송 동의를 받는 것도 추가해보면 재밌겠다.
  • 프로필 이미지 등록을 필수로 하지 말고 따로 분리해서 다루는게 더 유연할 것 같다는 생각이 들었다. 대략적으로 구현해두고 팀원과 상의해봐야겠다.

  • JWT를 이용해 권한부여를 적용하려다 후에 개발과정에서 테스트하는데 번거로울 것 같아 우선순위를 미뤘다.

  • member 객체가 갖고 있는 정보들 중 nickName, email, password는 필수값이고 자기소개 같은 선택사항은 따로 다룰 수 있도록 기능을 구현하기는 했지만 아래와 같이 하나의 메서드에 다 담아두고 각각 다른곳에서 다른 api요청을 받았을 때 처리하는 방식이 비효율적이진 않을까 의문이 들었다.

    • 영향이 아주 미미하기 때문에 별 상관 없다고 한다. (굳이 메서드 분리 안해도 됨)
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.SERIALIZABLE)
    public Member updateMember(Member member) {
        Member findMember = findVerifiedMember(member.getMemberId());

        Optional.ofNullable(member.getMemberNickName())
                .ifPresent(name -> findMember.setMemberNickName(name));
        Optional.ofNullable(member.getPassword())
                .ifPresent(password -> findMember.setEmail(password));
        Optional.ofNullable(member.getProfileImage())
                .ifPresent(image -> findMember.setProfileImage(image));
        Optional.ofNullable(member.getLocation())
                .ifPresent(location -> findMember.setLocation(location));
        Optional.ofNullable(member.getTitle())
                .ifPresent(title -> findMember.setTitle(title));
        Optional.ofNullable(member.getAboutMe())
                .ifPresent(aboutMe -> findMember.setAboutMe(aboutMe));
        return memberRepository.save(findMember);
    }

04/17(월)

api 문서화

  • FE 팀에서 response, request 포함시켜서 api문서를 달라고 요청 받았다. 아직 테스팅 + api 문서화 로직을 구현하지 못해 테스트용으로 사용하던 postman에 적어둔 내용을 그대로 복붙해서 보내줬다.
    • 이러면 수기로 작성해서 준거나 다름 없는데... api 문서화 기능 추가하는게 의미가 있나... 의문이 들었다.

서버 배포

  • 기본적인 기능구현이 완료되어 FE팀이 서버를 사용해보고 싶다고 요청 들어왔다. 처음에 ec2로 배포해 주소를 보내주려 했으나 윈도우 환경에서 이상하게도 local에서 ec2로 파일을 바로 올리는 방법(ec2서버 ssh키 받아와서 포함시키고 파일 전송 하는 방법)이 권한 오류로 계속 안된다.
    • 맥 환경을 사용하는 팀원이 똑같이 하니까 바로 된다;;;
  • 배포를 담당하는 팀원이 window 환경을 사용하기 때문에 어쩔 수 없이 방법을 바꾸기로 했다.

docker

  • docker을 이용해 빌드 파일을 이미지로 만들어 올려 컨테이너를 실행시키는 방식으로 하려고 하는데 db연결에 관해 의문점이 생겼다.
      1. ec2에서 mysql 설치하고 설정을 통해 해결하는 방법
      • 먼미래에 컨테이너 수량이 늘어남에 따라 db 일치시켜줘야하는 등 세팅이 번거로울 것으로 예상되었다.
      1. aws에서 지원하는 rds로 연결하는 방법
        • 일단 이게 쉬워보여서 이 방법으로 하기로 정했다.
        • but 과금 이슈 조심해야할 것 같다...

RDS

  • rds방식을 선택해 과금 이슈를 걱정할 수 밖에 없었다. FE팀의 자유로운 사용이 어렵기 때문에 추가적으로 ngrok을 이용해 서버를 제공하기로 했다.

ngrok

  • 하지만 ngrok을 이용한 방법은 아래와 같은 담점이 있었다.
  1. 애플리케이션을 계속 실행시켜야 한다.

    • BE팀원 중 한명이 배포해줘야 하기 때문에 (FE분들 ngrok이나 docker 사용하실 줄 모름) 배포 중에도 본인 작업을 해야하기 때문에 포트번호를 바꾸는 등 세팅 변경을 해줘야하고 터미널에서 돌리다보니 환경변수 설정도 따로 해줘야 한다. (기존에는 편하게 인텔리제이가 시스템 환경변수 가져와서 사용)

      • 하다하다 계속 오류 터져서 포기하고 yml파일에 비밀번호 직접 입력하고 build한 뒤에 다시 바꾸는 방식으로 했다... 인텔리제이 터미널, 파워쉘에서는 실행 잘되는데 우분투에서는 안되는 등... 어지러워서 원초적인 방법을 택했다.
    • 컴퓨터가 힘들어 한다 ㅠㅡㅠ

  2. 시간 제한이 있다.

    • 기본8시간 유지인데, 회원가입 후 키 등록해서 시간제한을 없앨 수 있다.


04/18(화)

JWT 토큰 검증 로직

MemberController에 JWT 토큰 검증 로직을 추가했다.
힘들게 찾아보면서 토큰의 Claims에서 값을 빼와 검증했는데 팀원이 같은 로직을 다른 엔티티컨트롤러에 적용한 것을 봤더니 너무 간단해서 자세히 알아봤다.

아래 첫번째 방식이 내가 사용한 방법이고 두번째 방식이 팀원이 사용한 방식이다.

첫번째 방식 (JwtUtil 사용)

@Component
public class JwtUtil {

    @Value("${JWT_SECRET_KEY}")
    private String secretKey;

    public String extractEmailFromToken(String token) {
        // "WishJWT " 부분 제거 (토큰만 남김)
        if (token.startsWith("WishJWT ")) {
            token = token.substring(8);
        }

        // secretKey를 Base64로 인코딩
        String encodedSecretKey = Base64.getEncoder().encodeToString(secretKey.getBytes(StandardCharsets.UTF_8));

        // 토큰에서 claims 가져오기
        Jws<Claims> claimsJws = Jwts.parser()
                .setSigningKey(encodedSecretKey)
                .parseClaimsJws(token);

        // 이메일 값 반환
        return claimsJws.getBody().get("memberEmail", String.class);
    }
}
@PatchMapping("/{member-id}/profile-image")
public ResponseEntity updateProfileImage(
        @PathVariable("member-id") @Positive long memberId,
        @RequestBody MemberDto.ProfileImage requestBody,
        @RequestHeader("Authorization") String token) {
    String userEmail = jwtUtil.extractEmailFromToken(token);
    Member member = memberService.findMember(memberId);
    member.setProfileImage(requestBody.getProfileImage());
    Member updatedMember = memberService.updateMember(member, userEmail);
    MemberJoinResponseDto responseDto = memberMapper.memberToMemberResponse(updatedMember);
    return new ResponseEntity(responseDto, HttpStatus.OK);
}
  • 장점
    • JWT 토큰에서 이메일을 추출하는 로직이 한 곳에 집중되어 있어 코드의 재사용성이 높다.
    • 토큰 관련 변경이 필요한 경우, JwtUtil 클래스만 수정하면 되므로 유지 보수가 쉽다.
  • 단점
    • JwtUtil 클래스를 사용하려면 빈 주입이 필요하므로 추가적인 설정 코드가 필요하다.

두번째 방식 (Authentication 객체 사용)

@PatchMapping("/{questionId}")
public Response<QuestionResponse> update(@PathVariable Long questionId,
                                         @RequestBody QuestionUpdateRequest request,
                                         Authentication authentication){
    QuestionDto questionDto = questionService.update(request.getTitle(), request.getBody(), authentication.getName(), questionId);
    return Response.success(QuestionResponse.from(questionDto));
}
  • 장점
    • Authentication 객체를 사용하므로 추가적인 유틸리티 클래스를 만들 필요가 없다.
    • Authentication 객체는 Spring Security가 관리하므로, 시스템의 인증과 관련된 변경이 있을 때 자동으로 적용된다.
  • 단점
    • 토큰에서 이메일을 추출하는 로직이 컨트롤러 메서드에 직접 작성되어야 하므로 재사용성이 떨어진다.

두 방식 모두 유효하지만, 전반적으로 첫 번째 방식은 토큰 처리 로직을 한 곳에 모아 놓기 때문에 코드의 재사용성과 유지 보수성이 높다는 장점이 있다. 반면, 두 번째 방식은 추가적인 클래스 없이 Spring Security의 Authentication 객체를 사용하여 간단하게 구현할 수 있다는 장점이 있다.

결론적으로, 프로젝트의 규모와 요구 사항에 따라 적절한 방식을 선택하면 된다. 코드 재사용성과 유지 보수성이 중요한 경우 첫 번째 방식을 사용하고, 간단한 구현을 원하면 두 번째 방식을 사용하면 좋다.

변수명 바꾸지 말자!

JWT 설정의 변수명 직관적이지 않은 것 같아서 바꿧다가 사용한 곳 찾느라 고생 좀 했다.

  • 변수명 변경은 최대한 지양하자(찾기 힘들다...)
  • 불가피하면 키워드 탐색 기능으로 열심히 찾자...

postman 꿀기능!

  1. 팀원을 초대해서 요청문을 함께 관리할 수 있다.

  2. Documentation 기능을 이용해 간력하게라도 문서화를 자동으로 할 수 있다.
    (restdoc으로 하느라 (테스트 코드 다 짜느라) 시간이 너무 많이 걸렸는데... FE분들은 빨리 필요하고... 힘든 와중에 너무 좋은 기능이다.)


04/19(수)

member api 문서화 완료

restdoc을 이용해 api 문서화를 완료했다. (풀코드 링크 참조)

작성 도중에 경로오류 때문에 고생했었다.

cors오류

  • 자세한 내용은 cors오류 링크 참조

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

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

	~~생략~~

    @Bean
    CorsConfigurationSource corsConfigurationSource() { // CorsConfigurationSource Bean 생성을 통해 구체적인 CORS 정책을 설정
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));   // 모든 출처(Origin)에 대해 스크립트 기반의 HTTP 통신을 허용하도록 설정
        configuration.setAllowedMethods(Arrays.asList("GET","POST", "PATCH", "DELETE"));  // 파라미터로 지정한 HTTP Method에 대한 HTTP 통신을 허용

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

	~~생략~~

}

이때 당시에는 RESTfull한 API를 디자인하기 위해서 "GET","POST", "PATCH", "DELETE"만 사용해야 한다고 REST API의 정의에 대해 잘못알고 있었다.


04/20

이미지 처리하기

  • 추가적인 부분이지만 이미지는 URL 타입으로 저장하는게 훨씬 부담이 적다. base64형태의 이미지를 URL로 변환해 저장할 수 있도록 하면 좋을 것 같다.
  • 클라이언트측에서 디코딩해서 보내주는게 통신 트래픽 줄이고 좋을 것 같다.

04/21

회의록 (통합회의)

혜나님과 함께 작업하며 생겼던 이슈들이 앞으로 다른 FE팀원분들에게도 비슷한일이 생길 수 있어서 알게된 정보 공유 겸 정해야할 부분에 대해서 의논하고자 해서 회의를 제안했습니다. 참여해주셔서 감사합니다.

오리진이 다를 때 cors 오류가 발생할 것 같아서 서버에서 전체 허용을 해뒀지만
브라우저가 오리진이 다르면 프리플레이트를 자동으로 보내서 OPTIONS 타입의 요청을 서버로 보냅니다.
OPTIONS에 대한 요청처리가 서버에 구현되어 있지 않아서 서버에서는 OPTION 타입을 차단해 두었고 이러한 오류가 cors로 나타났었습니다,
본래 cors란 동일출처가 아닐경우 위험한 접근일까봐 차단하는 것인데, 전혀 다른 이유로 오류가 발생했던 것입니다.
따라서 cors와 같이 서버와 연관된 오류가 발생했을 때 빠르게 해결하기 위해서 request헤더, 화면공유를 적극적으로 요청해주셨으면 좋겠습니다.
그리고 이에 대한 해결방안은 크게 2가지가 있을 것 같습니다. FE가 해결 or BE가 해결입니다.

  1. cors 등 문제가 발생했을 때 저에게 연락하시면 되는데, 저는 척보면 척할 실력이 안되니 요청과 함께 정보도 많이 부탁드립니다. ㅠㅡㅠ

  2. 혜나님이 http proxy middleware 방식으로 해결하셨는데 "모든" 요청에 적합한 방식이 아니기 때문에 추가적인 해결이 필요해 보입니다. FE 아이디어 부탁드립니다.

    • 그냥 ngrok 문제였다. ec2로 하니까 모든게 해결 됨.
  3. BE의 해결방법은 각 엔티티 컨트롤러마다 OPTIONS처리 로직 구현
    (but 배운적이 없어서 잘모릅니다.. 일단 구글링해서 작성해보긴 했는데 제대로 작동할지 모르겠습니다.)
    작동 안함 ㅠㅡㅠ

  4. 저는 FE분들과 얘기도 안해보고 3-Tier 아키텍쳐를 당연시 했는데, 어제 혜나님 작업방식을 보니 현재 구조는 클라이언트 - 서버(FE) - 서버(백엔드서버) - DB 구조였습니다.

    • 3-Tier 아키텍쳐 맞았음 클라이언트(리액트서버) - 서버 - db 구조임
  5. ngrok서버는 닫았다 열 때마다 url 주소가 바뀝니다. 주의해주세요.

  6. 백엔드 팀원이 request를 요청할 때 FE분들 기준의 엔드포인트는 필요가 없습니다. postMan으로 작성하는 url 기준으로 공유 바랍니다.

  7. 이 밖에도 프론트측과 백엔드가 함께 대화가 필요한 내용이 있을 것 같은데 어떤 기술을 적용하실 때(자동으로 적용될 수도 있습니다 ㅠ 이것도..함꼐) 확인하셔서 회의를 제안해주시면 좋을 것 같습니다.


ngrok byebye

프론트분과 함께 고생했던 cors 오류를 해결하기 위해 OPTIONS 로직을 추가하는 등 다양한 방법을 사용했지만 해결되지 못했다.

혹시나 싶어 ec2에 또 배포했더니 ec2에서는 OPTIONS 로직 없이도 문제없이 동작한다.

ngrok안쓰기로 정했다. 앞으로 ec2만 쓸 것이다.

ec2 RDS 연결 과정에서 과금 이슈가 발생할까 겁나서 꼼꼼히 확인하고 정리해뒀다.


스키마 수정

  • answer vote, questrion vote 추가
  • 변수명 변경

api 문서화 합치기

postman 문서화 기능을 이용해 웹 페이지에서 간편하게 볼 수 있도록 다른 팀원이 작성한 문서화 내용과 합쳤다. (캡쳐본 링크 참조)


04/22(토)

Oauth2 문제...

기존 코드가 Oauth2 추가에 대한 대비가 전혀 되어있지 않았다.
Oauth2 객체를 따로 관리하기 위해서는 아래와 같이 매개용 객체를 만들었어야 했는데... 이제와서 깨달아 버렸다.

기존 테이블

변경해야하는 테이블

  • 참고로 객체관점의 스키마지만 팀원과 필드명이나 구조에 대한 협의가 되지 않아 대략적으로 id로만 연결을 표현해둔거다.

이제와서 매개용 객체를 추가하면 관련된 기능들을 전부 다 손봐야하는데 사실상 2일밖에 시간이 남지 않았다...

어쩔 수 없이 기존 member 객체에 password를 더미객체로 채워넣는 방식으로 만들어야할 것 같다.

문제는 순환참조 오류가 발생한다는 것이다.

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

Member 객체와 Oauth2 객체가 필요한 기능들이 비슷하다 보니 Oauth2 설정에 Member기능을 가져와 쓰다보니 발생했다.

어쩔 수 없이 코드의 중복이 발생하지만 아래와 같이 정보 저장방법을 수정해 해결했다.

    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;
    }

04/23(일)

OAuth2 로컬 개발 완료

로컬에서는 정상적으로 작동한다.
하지만 문제가 있다. 매개용 객체를 만들지 않아서 구글로 로그인하면 password 컬럼에 null값이 들어있다. 한번 로그인한 후로는 password를 입력하지 않고 이메일만 입력해도 로그인이 되어버리기 떄문에 더미 데이터를 채워주던지 google 로그인으로 저장된 회원인지 검증 가능한 로직을 추가해야할 것 같다.

또 cors문제..

이번에는 그래도 비교적 간단했다.

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

아래와 같이 cors 설정을 수정해 해결했다.

    @Bean
    CorsConfigurationSource corsConfigurationSource() { // CorsConfigurationSource Bean 생성을 통해 구체적인 CORS 정책을 설정
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST", "PATCH", "DELETE", "OPTIONS"));  // 파라미터로 지정한 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;
    }

04/24(월)

OAuth2 회원가입 회원 일반로그인 못하게 변경

OAuth2 회원가입 회원들의 password를 더미데이터로 채워서 관리하려고 했는데, 그냥 비밀번호가 null인 경우 회원가입 못하도록 막는게 더 좋을 것 같다.

회원가입시 유효성 검사에서 null값 제외시키는 것은 클라이언트측에서 담당해주면 될 것 같다. (물론 더블체킹할 수 있게 만들면 더 좋지만 기간이 얼마 안남아서... 빠르게 할 수 있는 방향으로 선택한거다.)

통합 회의

  1. 진행상황 공유 (+ 깃허브 push 해주세요!)
  2. FE팀 배포방식?
    • AWS S3 이용해서 배포하신다고 함
  3. 테스트 체크리스트 작성에 관하여..

대략적으로 짜본 스케쥴
화= 개발 마무리
수 = 배포완료
목 = 체크리스트 작성


04/25(화)

또 cors...

nogrok에서 계속 cors나서 ip 주소 받아서 명시해 보는 등 조치를 취해봤지만 계속 안됐다.
같은 버전의 서버파일을 ec2에 업데이트 후 테스트하니 문제없다...

다른팀을 도와주며 느낀점

다른 백엔드 팀에서 작업 중 막히는 부분이 있다고 도와달라고 종종 요청이 왔었는데 오늘은 3팀에서 요청을 받았다. JWT, 배포, 쿼리문 다양하게 질문을 받았는데 작업 후반부에 오니 세팅의 차이가 너무 심해서 대부분 적절한 해결방법을 찾아내지 못해서 도움이 되어드리지 못했다.

특히 JWT의 경우 인증과정이 제대로 수행되지 않아 로그인 시 토큰 발급이 안되는 상황이였는데 기능분리가 제대로 되어있지 않고, 코드의 중복이 심해서 security에서 자동으로 호출하는 메서드들이 이곳저곳에서 오버라이딩 되어있는 등 숨은그림 찾기가 매우 어려웠다... @Configuration 클래스가 서로 중복되거나 충돌하는 등 일부 문제점을 찾고 수정을 돕기는 했지만 2시간정도 시간동안 시도하다 결국 완벽하게 해결하지는 못하고 포기하게 되어 너무 아쉬웠다.

오늘의 경험 덕분에 코드의 가독성, 확장성, 기능분리 등의 중요함을 다시한번 느낄 수 있었다.

엑세스 토큰 재발급

리프래쉬 토큰을 이용해 엑세스 토큰을 재발급하는 로직을 추가해 뒀었는데 프론트에서 해당 기능을 적용하는데 어려움을 겪고 있다고 해서 함께 고민해 봤다.

cf. refresh 토큰도 토큰헤더를 붙이는게 좋다는걸 알고 있지만 편하게 구분하시라고 헤더를 빼는 방향으로 구분지었다.

기존에 로그인을 통해 각 토큰을 브라우저에 받는 것을 문제없이 해결하신걸 보면 각 정보의 변수명을 구분 못하신건 아닐텐데 리프래쉬 토큰을 Authorization = 리프래쉬 토큰과 같이 서버로 전송하셔서 제대로 동작하지 않았던 것이였다.

@RestController
@RequestMapping("/refresh")
@AllArgsConstructor
public class RefreshController {
    private final JwtTokenizer jwtTokenizer;

    @PostMapping
    public ResponseEntity<String> refreshAccessToken(HttpServletRequest request) { // 리프레쉬 토큰 받으면 엑세스 토큰 재발급
        String refreshToken = request.getHeader("Refresh");
        if (refreshToken != null) { // && refreshTokenHeader.startsWith("WishJWT ") 나중에 추가
//            String refreshToken = refreshTokenHeader.substring(8);
            try {
                Jws<Claims> claims = jwtTokenizer.getClaims(refreshToken, jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey()));

                String email = claims.getBody().getSubject();
                Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes());
                String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());

                String accessToken = jwtTokenizer.generateAccessToken(claims.getBody(), email, expiration, base64EncodedSecretKey);
                return ResponseEntity.ok().header("Authorization", "WishJWT " + accessToken).body("Access token refreshed");
            } catch (JwtException e) {
                return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid refresh token");
            }
        } else {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Missing refresh token");
        }
    }
}

위 코드와 같이 request.getHeader("Refresh")!=null로 되어 있기 때문에 변수명이 달라 서버가 리프래쉬 토큰값을 인식하지 못한 것이다.

이번에는 다행히 내가 도움이 될 수 있었지만 프론트 문법을 몰라서 로직 관련된 부분은 이해하기 힘들었다. 후에 간단하게라도 공부해서 이해도를 높이면 협업 과정에서 큰 도움이 될 것 같다.


04/26(수)

OAuth2 배포

위와 같이 리다이렉트 미스매치라고해서... 보내줘야하는 클라이언트의 주소가 적절하지 않거나 권한이 없나 했더니...
구글 입장에서는 나의 백엔드 서버가 클라이언트니까... 나의 서버에 권한이 없는거였다 ㅠㅡㅠ
이제 아무곳에서나 접속해도 로그인창 정상적으로 보인다.

클라이언트에서 구글인증에 대한 처리를 추가못해서 정확한 확인은 못했지만
아래와 같이 배포환경에서도 정상정으로 동작을 수행하는 것 같다.


04/27(목) 마지막날

개발자테스트

(위 켭쳐본은 전체 기능 중 일부분입니다.)

만들다보니 자잘한 기능들이 생각나서 추가하게 되는 경우가 많았다.
이러한 자잘한 작업들이 모여 생각보다 많은 시간을 허비하게 되었고 달성률이 아쉽게 나왔다.

그래도 협업과정을 연습해보는 기회로 충분히 좋은 경험이 된 것 같다.

최종 결과물

0개의 댓글