no image
DTO의 반환 형태, 명명법에 대해
[글의 의도] 1월 30일, 야놀자 패캠 부트캠프를 수료했다. 취업 준비를 하면서 블로그 글을 쓰기 위해 주제를 고민하던 중 최근 파이널 프로젝트에서 많이 사용했고, 개인 프로젝트에서 한번 더 고민한 DTO에 대해 글을 쓰려고 한다. [DTO의 형태] 파이널 프로젝트는 FE + BE가 함께 진행하며 개발을 했으므로 나는 API Server만 개발을 했다. 따라서 FE로 어떤 값을 어떻게 보내야 편하게 사용할 수 있을지 고민을 팀원들과 했었다. 그 중 고민이 되었던게 DTO의 형태였다. 값을 어떻게 내려보낼지 고민하기 전에 DTO의 이름도 관련이 있으니 먼저 살펴보자 [ 자연스럽게 사용하게 된 DTO의 네이밍 ] 미니 프로젝트, 토이 프로젝트를 진행하면서 팀원분들의 코드로 어깨 넘어 배운 DTO 명명법은 ..
2024.02.05
no image
MemberServiceImpl 인터페이스에 대하여
[글의 의도] 공부할 양이 너무 많다는 걸 깨닫고 방황하며 이리저리 패스트캠퍼스 강의를 듣다가 어디선가 주워 들어본 내용의 '유지보수하기 좋은 코드 디자인 ' - '적절한 객체의 크기를 찾아가는 여정' 강의를 우연히 들었다. 강의 내용을 어디서 들어봤나 싶었는데 4개월 전 부트캠프 초반 멘토님께서 과제를 리뷰해줄 때 들었던 내용과 일치하여 블로그에 작성해보기로 했다. 강의의 주 내용은 흔히 개발을 할 때, Controller, Service, Repository 계층으로 개발을 진행한다. 이 때 MemberService를 인터페이스로 선언하고 구현체로 MemberServiceImpl을 사용했었는데 이에 관한 인터페이스 내용이다. [가물가물한 멘토님의 피드백 내용] 내 기억속에 KakaoAPI를 활용한 도..
2023.12.20
no image
패스트캠퍼스X야놀자: 미니프로젝트(숙박 예약 서비스) 후기
이번 미니 프로젝트는 여러 개의 조에서 동일한 주제로 FE 수강생들과 협업 프로젝트를 진행한다. 기존에는 BE 수강생들 끼리 API를 설계하는 주제를 가지고 프로젝트를 진행했었는데 FE 수강생들과 협업하는 경험은 쉽게 경험하지 못할 좋은 기회인 것 같다. [싱크업 미팅] BE 수강생 4명이서 한 팀을 이루었고 우리 조는 사다리 타기로 조장을 선정했다. 이후에 데이터베이스 설계와 API 설계를 진행했다. [나의 역할] 팀원 모두 초반에는 요구사항을 분석하고 도메인을 설계하며 공통적인 작업을 진행했고 이후에는 역할을 나눠서 따로 개발을 진행했다. 나는 로그인과 회원가입, Spring Security 인증, 인가를 담당했다. 이 주제는 현업에서 비교적 신입에게 맡기지 않는 분야이긴 하지만.. 그래도 한 번 맛..
2023.12.19
no image
SpringSecurity AuthenticationEntryPoint 동작하지 않았던 이유
[글쓰는 의도] 미니프로젝트 과정에서 스프링 시큐리티를 담당했다. JWT AccessToken을 인증하는 과정에서 분명 AuthenticationException 에러가 발생하면 AuthenticationEntryPoint가 동작해서 예외를 처리하기 마련인데 아무리 찾아봐도 내가 커스텀한 AuthenticationEntryPoint 는 동작하지 않았다. [여러가지 해결방안] AI와 열띤 토론을 한 결과 해결책은 다음과 같았다. 1. 커스텀한 예외가 AuthenticationException을 상속받지 못한 경우 public class BadTokenException extends AuthenticationException 2. Spring Bean으로 커스텀한 AuthenticationEntryPoint..
2023.11.27
no image
프로젝트 도중 마주친 @Value의 주의사항
[글 쓴 의도] 이번 토이 프로젝트 주제는 이전에 진행했던 동일 주제에 로그인, Open API, 회원가입 등 다양한 기능을 추가하는 방향으로 진행하기로 했다. 내가 맡은 부분은 Kaka Open API를 연결하고, 여정을 등록하는 과정에서 장소 이름을 입력하면 도로명 주소로 변환하는 기능을 구현하기로 했다. Key 값을 외부에 노출하지 않기 위해 .env 파일에 KAKAO_KEY 필드를 추가하고 application.yaml 파일에 api.key 항목을 추가해서 @Value 어노테이션으로 관리하려고 했다. 하지만 @Value 어노테이션이 잘 동작하지 않는 것을 확인했다. 이유를 알아보자 [문제가 발생한 이유] @Value를 썼을 때, 인텔리제이가 아무 잔소리 없이 넘어갔다는 것은 매핑은 잘 되었다고 볼..
2023.11.13
no image
application.yaml 파일 .env 설정
[글을 쓰는 의도] 야놀자 패캠 부트캠프에서 팀 단위로 하는 토이 프로젝트 마다 벨트를 꽉 매고 버스를 타고있다. 다음 프로젝트를 위해 개인 프로젝트를 만들어서 Spring JPA를 복습하려고 한다. 간단하게 프로젝트를 만들고 MySQL과 JPA를 연동하는 과정에서 애를 먹어서 나중엔 고생을 덜 하려고 글로 남기려고 한다. [막힌 부분] 먼저 Spring Init에서 간단한 의존성을 추가하고 프로젝트를 만들었다. 그리고 MySQL을 Docker-compose를 사용해서 DB를 만들었고 이 과정에서 지난 팀 프로젝트에서 썼던 외부 .env 파일을 사용해 docker.yml, .env , application.yaml을 설정했다. //application.yaml spring: config: import: ..
2023.11.08
no image
패스트캠퍼스X야놀자: 백엔드 개발 부트캠프_토이프로젝트2 후기
Title : 여행 여정을 기록과 관리하는 SNS 서비스 2단계 2단계 토이프로젝트는 한 달 전 팀원들과 동일하게, 비슷한 주제로 Spring을 가지고 진행했다. 1단계 서비스와 비슷한 요구사항을 가지고 특징은 API를 설계하고 개발하는 것이 중요하다고 생각했다. 워낙 팀원분들이 잘 하시는 분들이여서.. 이번에도 배운다는 마인드로 임했다. 1단계에서는 여정 부분을 개발했다. 2단계에서도 역할을 나누기 위해 랜덤으로 역할을 정하고 나는 여정 부분을 맡게 되었다. 개발 일정 개발 일정은 위 사진과 같이 동일하게 진행했다. 첫 날에는 API 설계 및 ERD 설계를 진행하고 팀원 역할을 배정했다. 나머지 일정도 위 계획표와 비슷하게 동일했다. API 설계 (여행, 여정) API 설계는 위에 사진과 같이 진행했다..
2023.11.02
no image
프로그래머스 오답노트 (개인정보 수집 유효기간)
프로그래머스 코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요. programmers.co.kr https://school.programmers.co.kr/learn/courses/30/lessons/150370 import java.util.*; import java.time.*; class Solution { public LocalDate func(int year, int month, int day, int terms){ LocalDate days = Year.of(year).atMonth(month).atDay(day); return days.plusMonths(terms); } public int..
2023.10.19
no image
패스트캠퍼스X야놀자: 백엔드 개발 부트캠프_토이프로젝트 후기
Title : 여행 여정을 기록과 관리하는 SNS 서비스 1단계 이번 토이프로젝트는 조를 새로 배정 받아서 5인 1조로 과제를 수행했다. 첫 조원분들과 만나는 자리에서 회의를 간단하게 진행했는데 다 프로젝트 경험이 여러 번 있으시고, 관련 지식도 많은 팀원 분들을 만났다. 토이프로젝트 진행은 화면에 출력하는 view 1명, 여행관련 2명, 여정관련 2명으로 나눠서 진행했고 나는 여정 파트를 맡았다. 개발 일정과 프로젝트 기획은 아래와 같다. 제안서에는 여행속에 여정이 배열로 존재하는 구성이었는데, 우리 조는 여행 파일 따로, 여정 파일 따로 관리하기로 하였고, 여정을 추가, 읽기 위해서는 여행 파일의 인덱스가 필요하다는 특징이 있다. 그렇게 구조를 설계하고 각자 개발에 들어갔다. 나는 이번이 여럿이서 하..
2023.09.15

[글의 의도]

  1월 30일, 야놀자 패캠 부트캠프를 수료했다. 취업 준비를 하면서 블로그 글을 쓰기 위해 주제를 고민하던 중 최근 파이널 프로젝트에서 많이 사용했고, 개인 프로젝트에서 한번 더 고민한 DTO에 대해 글을 쓰려고 한다. 

 

[DTO의 형태]

 파이널 프로젝트는 FE + BE가 함께 진행하며 개발을 했으므로 나는 API Server만 개발을 했다. 따라서 FE로 어떤 값을 어떻게 보내야 편하게 사용할 수 있을지 고민을 팀원들과 했었다. 그 중 고민이 되었던게 DTO의 형태였다. 값을 어떻게 내려보낼지 고민하기 전에 DTO의 이름도 관련이 있으니 먼저 살펴보자

 

[ 자연스럽게 사용하게 된 DTO의 네이밍 ]

 미니 프로젝트, 토이 프로젝트를 진행하면서 팀원분들의 코드로 어깨 넘어 배운 DTO 명명법은 어미에 --Response를 붙이는 형식이었다. 아마 --DTO라고 대문짝만하게 안 붙여도 현업에서는 다 DTO임을 알고 사용한다고 그랬던 것 같다. 그렇게 나도 사용을 하고있었다.

 

[ List<DTO> vs 단일 객체 DTO]

 파이널 프로젝트 진행 중, 코드 리뷰를 진행하면서 한 팀원 분의 DTO가 나와 다른 형태임을 발견했다. 다음 상황을 살펴보자

 

-  Controller에서 전체 게시글 조회 요청에 대한 응답을 주는 상황

public record PostsResponse(
		String title,
        	String content,
        	String author
){
}
public List<PostsResponse> getPosts(){

}

 

 보통 List 형식의 응답을 내릴 때 나는 위와 같은 방식으로 record를 작성 했던 것 같다. 하지만 팀원분은 List로 담아서 내리면 PostsResponse를 여러 개 응답한다는 느낌을 받아서 아래와 같은 형식으로 사용하셨다.

 

public record PostsResponse(
		List<Post> postList
){

	private record Post(
    	String title,
        String content,
        String author
    ){
    }
}
public PostsResponse getPosts(){

}

 

 두 방식의 큰 차이는 내부 클래스(record)를 사용하냐 VS 안하냐의 차이인 것 같다. 어떤 것이 더 나은 방향인지 미리 말하자면 2번째 방식이 더 나을 것이다. 이유를 알아보자.

 

[ 가독성 VS 확장성 ]

 첫 번째 방법의 경우 장점은 가독성이라고 말할 수 있을 것 같다. 보통 다른 사람이 작성한 메서드를 볼 때 메서드 이름과, 반환 타입, 매개 변수 등 여러가지 요소를 고려하여 해당 메서드의 역할을 유추할 수 있다. 위 코드의 예시처럼 반환 타입을 List로 주게 되면 해당 기능은 여러 개를 응답한다고 직관적으로 유추할 수 있을 것 같다. 만약 Response만 딸랑 반환하게 되면 해당 Response를 직접 들어가서 봐야하는 조그만 수고가 있을 수 있다고 생각한다. 단점은 두 번째 방법의 특징을 나열하면서 알아보자.

 

두 번째 방법의 경우, 장점은 확장성이 열려있다는 점이다.  위 상황의 경우에는 Post 게시글 하나 만 응답을 주고 있다. 현재 상황에서는 문제가 없어보인다. 하지만 게시글로 끝나는 것이 아닌, 댓글, 태그, 이미지 등 추가적인 정보에 대한 확장이 열려있다. 두 번째 방법의 경우에는 간단하게 내부 레코드에 필드만 추가해주면 쉽게 확장이 가능하다. 반면에 첫 번째 방법의 경우 여러 부분의 수정을 피할 수 없을 것이다.

 

일반적으로 봤을 땐 특별한 상황이 아니라면 두 번째 방법이 좋아보이긴 한다. 하지만 첫 번째 방법을 사용하면, 게시글을 독립적으로 사용할 때 유용할 수 있다. 독립적으로 사용할 때 유용한 사항은 아래와 같다. (Chat GPT 참조)

개별 처리 필요: 클라이언트에서 게시글을 개별적으로 처리해야 하는 경우입니다.
예를 들어, 클라이언트가 각 게시글을 독립적으로 화면에 표시하거나, 
각 게시글에 대해 별도의 연산을 수행해야 하는 경우 
각 게시글을 독립적인 객체로 다루는 것이 유리합니다.

독립적인 데이터 변경: 게시글이 독립적으로 추가, 삭제, 수정되는 경우입니다. 
각 게시글이 독립적인 객체로 관리되면, 특정 게시글에 대한 변경 사항만을 쉽게 반영할 수 있습니다.

세부 정보 요구: 각 게시글에 대한 세부 정보를 요구하는 경우입니다. 
예를 들어, 게시글의 작성자, 작성 시간, 조회 수 등 추가 정보를 필요로 하는 경우,
이런 정보를 각 게시글 객체에 포함시킬 수 있습니다.

비동기 처리: 클라이언트가 비동기적으로 각 게시글을 처리해야 하는 경우입니다. 
예를 들어, 웹 페이지에서 스크롤을 내릴 때마다 게시글을 하나씩 불러오는 '무한 스크롤' 기능을 구현하는 경우,
각 게시글을 독립적인 객체로 다루는 것이 효율적입니다.

 

 

[느낀 점]

멘토님이 말씀해주신 `진리의 회바회 팀바팀 사바사` 가 떠오르는 부분인 것 같다. 클라이언트 요청에 따라 정하자.  

개발에는 정답이 없다. 아무리 잘나가는 기술이라도, 내게 주어진 환경에 맞지 않다면 필요가 없을 것이다. 코드를 작성하거나 기술을 사용할 때, 왜 필요한지 다른 방안은 없는지, 방안이 여러 개라면, 이들의 차이가 무엇인지 다시 생각해보는 습관을 가져보자. 

[글의 의도]

 

 공부할 양이 너무 많다는 걸 깨닫고 방황하며 이리저리 패스트캠퍼스 강의를 듣다가 어디선가 주워 들어본 내용의

'유지보수하기 좋은 코드 디자인 ' - '적절한 객체의 크기를 찾아가는 여정' 강의를 우연히 들었다. 강의 내용을 어디서 들어봤나 싶었는데 4개월 전 부트캠프 초반 멘토님께서 과제를 리뷰해줄 때 들었던 내용과 일치하여 블로그에 작성해보기로 했다. 강의의 주 내용은 흔히 개발을 할 때, Controller, Service, Repository 계층으로 개발을 진행한다. 이 때 MemberService를 인터페이스로 선언하고 구현체로 MemberServiceImpl을 사용했었는데 이에 관한 인터페이스 내용이다.

 

[MemberService]

 

[가물가물한 멘토님의 피드백 내용]

 

 내 기억속에 KakaoAPI를 활용한 도서 검색 토이프로젝트를 주제로 한 피드백 멘토링 시간이었다. 멘토님께서 우리 그룹스터디의 팀원 한 명씩 개인 과제를 리뷰하면서 이야기를 나눴던 기억이 난다. 그 때 상황은 아마 팀원중에서 BookService를 개발할 때, BookServiceImpl로 클래스 명을 지은 코드를 보고 멘토님께서 ~~Impl 명명법은 별로 좋지 않다. 정도로만 기억을 하고있다. 그 당시엔 끄덕끄덕하고 지나갔지만 지금 다시 생각해보니 뭐라 하셨는지 기억이 안난다.. 나는 그저

 

김영한님 강의에서 Impl을 쓰는 구나.. 어 팀원분도 이런 명명법을 쓰셨구나.. 어 멘토님이 쓰지 말라시네.. 이런 몹쓸 메모리만 남았다.

 

 다시 기억을 되찾고자 멘토님께서 남기신 코멘트를 찾았고 멘토님의 자세한 리뷰는 다음과 같았다.

 

[멘토님 리뷰 발췌]

 

 

 

[인터페이스를 왜 쓸까?]

 

[간단한 결제 시스템 구조 (패스트캠퍼스)]

 

 강의에서 예시로 드는 상황은 위 사진과 같다. PaymentController가 실제 구현체가 아닌 인터페이스 PaymentService를 참조하는 이유는 다음과 같다.

 

1. 세부 구현체를 숨기고 인터페이스를 바라보게 함으로써 클래스 간의 의존 관계를 줄임.

2. PaymentService를 구현하는 여러 구현체가 있고 기능에 따라 적절한 구현체가 들어가 다형성을 주기 위함 

 

만약 PaymentController가 구현체를 참조한다면, 구현체의 개수가 늘어날 수록 맺어야하는 의존 관계가 늘어난다. 그러므로 의존성이 지나치게 많아지니깐 인터페이스를 사용해서 확장, 유지보수에 장점을 가진다. PaymentController 입장에서신한, 우리 등등 누가 어떻게 구현을 하든 pay()라는 기능만 수행할 수 있으면 장땡인 것이다.

 

강의는 이 부분에서 대체성이라는 단어를 소개했다. 이 구조에서 PaymentService는 책임이 명확하다. 다들 pay()라는 기능을 수행하는 책임이 있다. 책임이 명확하면 대체성이 높다. 이 말은 대체성을 띄고 있다는 말로 해석된다. 즉 쉽게 대체가 가능하다 == 맡은 책임이 명확하다 로 볼 수 있다. 

 

 

 

[역할, 책임이 너무 많은 인터페이스]

 

[MemberService 예제]

 

 강의에서 보여준 MemberService 예제는 위 사진과 같다. MemberService 에서는 Member 와 관련한 모든 동작을 정의하고 있다. 

 

1. findById

2. findByEmail

3. changePassword

4. updateName

 

MemberService가 정의하는 위 4가지의 메서드는 아래 2개의 기준으로 분류를 할 수 있을 것 같다.

 

- CRUD 관점 (작성, 조회, 수정, 변경, 삭제)

-  2개 이상의 방법으로 구현이 가능한지? 유일한 방법인지.. 다른 대안이 있는지..

 

첫 번째 관점으로 메서드를 분류 하면 다음 표와 같다.

조회 findById, findByEmail
수정 updateName
변경 changePassword

 

 수정, 변경을 같이 둘 수도 있지만 일단 이렇게 설정을 해두고 진행하자. 이렇게 두고 본다면 MemberService 인터페이스를 MemberFindService, MemberSignUpService, MemberEditService 와 같이 여러 개로 분리함으로써 모호한 MemberService의 책임 범위를 세세하게 쪼갤 수 있을 것이다.

 

두 번째 관점으로 다른 여지가 없는? 인터페이스를 찾아보자. 이 부분은 예제임으로 극단적으로 판단해서 진행해보자.

 

[MemberService 인터페이스 분리 (출처 : 패스트캠퍼스)]

 

 

 1번, 2번 그리고 4번 메서드의 경우 고유한 id, Email 값으로 Member를 찾는 역할을 한다. 이는 대체할 방법이 없다고 볼 수 있다. 여기서 대체할 방법이 있다 없다를 예로 들면 (집 -> 학교 가능 방법) 을 생각해 봤을 때 걸어서 가거나, 버스를 타거나, 지하철을 타거나 여러 방법을 생각할 수 있는데 1번, 2번, 4번의 경우는 살짝 극단적이지만 대체할 방법이 없다고 볼 수 있다.

 

 즉 대체할 방법이 없는 메서드들은 인터페이스로 선언하는 것이 아닌 그냥 바로 구현체로 설정해도 괜찮을 것 같다는 의견이다. 

public class MemberFindService{

	
    public Member findById(Long id){
    
    }
    
    public Member findByEmail(Long id){
    
    }
}

 

 

 반면에 changePassword의 경우 대체할 경우가 2개 이상 존재한다고 볼 수 있다. 현재 비밀번호로 변경을 한다던지, SNS 연동을 통해 인증을 하고 변경을 한다던지.. 여러 가지 방법으로 비밀번호를 변경할 수 있다고 볼 수 있다. 그렇기에 changePassword는 구현체가 아닌 인터페이스로 개발을 진행할 수 있을 것 이다.

 

public interface ChangePasswordService{

	void change(Long id, PasswordChangeRequest dto);
}




//////////////////////// 인증 방식
public class ByAuthChangePasswordService implements ChangePasswordService{

	@Override
    public void change(Long id, PasswordChangeRequest dto){
    
    }
}

//////////////////////// 비밀번호 방식
public class ByPasswordChangePasswordService implements ChangePasswordService{

	@Override
    public void change(Long id, PasswordChangeRequest dto){
    
    }
}

 

 

[결론]

 

 내가 개발하는 프로젝트의 인터페이스와 예제의 MemberService 인터페이스는 너무 많은 책임을 지고있는지도 모른다.

해당 글을 쓰면서 정확하게 모두 작성하진 못했지만, 어느정도 인터페이스를 설계할 때 주의해야 할 점에 대해 알게된 것 같다. 

 

 

서비스 인터페이스를 쪼개라 -> 책임을 명확하게 해라 -> 행위 기반으로 서비스를 표현해라

 

다른 대체제가 없다면 인터페이스로 선언하지 말고 구현체로 선언해라

 

인터페이스 이름으로 역할, 책임을 표현할 수 있다.

 

 

 

이번 미니 프로젝트는 여러 개의 조에서 동일한 주제로 FE 수강생들과 협업 프로젝트를 진행한다. 기존에는 BE 수강생들 끼리 API를 설계하는 주제를 가지고 프로젝트를 진행했었는데 FE 수강생들과 협업하는 경험은 쉽게 경험하지 못할 좋은 기회인 것 같다.

 

 [싱크업 미팅]

 

 BE 수강생 4명이서 한 팀을 이루었고 우리 조는 사다리 타기로 조장을 선정했다. 이후에 데이터베이스 설계와 API 설계를 진행했다. 

 

[ERD 다이어그램]

 

[API 명세서]

 

 [나의 역할]

 

 팀원 모두 초반에는 요구사항을 분석하고 도메인을 설계하며 공통적인 작업을 진행했고 이후에는 역할을 나눠서 따로 개발을 진행했다. 나는 로그인과 회원가입, Spring Security 인증, 인가를 담당했다. 이 주제는 현업에서 비교적 신입에게 맡기지 않는 분야이긴 하지만.. 그래도 한 번 맛을 봐보자는 이유와 협업 능력을 키우는 것을 목표로 해당 프로젝트에 임했다.

 

 

[미니 프로젝트 역할 담당]

 

[FE 수강생과의 협업]

 

 API를 설계하고 개발하면서 회원가입, 로그인을 담당하는 FE 수강생 분과의 의견을 나누는 과정이 있었다. 이번 프로젝트로 느낀 점은 크게 2가지가 기억에 남는다.

 

1. API 명세를 명확하게 세우자

2. 몰라도 자신감 있게 하자

 

 첫 번째로 API 명세의 중요성을 느꼈다. 기존 간단한 프로젝트를 진행할 때는 나 혼자 진행했거나, 문서화에 중요를 두지 않고 막무가내로 진행했었다. 아니나 다를까 협업 과정이 원활하지 못했다.

 

[API 명세 관련 소통]

 

 템플릿으로 정리된 API 명세를 처음 세워보는 상황이었기에 그저 팀원분께서 사용하신 이전 템플릿을 복사해와서 Request, Reponse 필드 종류, 네이밍 규칙, Header 등 API를 통신할 때 사용할 규칙을 신중하게 고려하지 않고 엉성하게 작성했던 것.. 안좋은 버릇을 고쳐야할 것 같다.

 

 두 번째로는 프로젝트를 진행하면서 자신감이 많이 부족했던 것 같다. 같은 조의 어떤 팀원분은 자기는 잘 모른다고, 처음이라고 소개를 하신 분이 있었다. 나는 처음이라고 우물쭈물 쭈뼛쭈뼛 하고 있을 때, 그 분은 FE, BE 통합 회의에서도 명확하게 자신의 의견을 전달하고 틀린 부분을 배우는 자세를 가지고 성장하는 느낌을 받았다. 이번 미니 프로젝트를 돌아보면 나는 잘 모른다는 이유, 처음 해본다는 이유로 쫄았던 것이 부끄러워졌다. 쫄지 말고 실수 해도 괜찮다는 마인드를 장착했다.

 

[차두리 명언]

 

 

 [미니 프로젝트가 끝나고]

 

 미니 프로젝트에서 Git을 통한 협업 관리와 Flow, Issue 를 작성하는 방법을 배웠다. 이번 프로젝트에서는 로그인, 회원가입 기능을 담당해서 개인적으로 도메인 설계보다는 Security를 공부하느라 ERD 관계, 도메인 설계를 잘 집중하지 못했던 것 같다. 그렇기에 전부터 생각해온 개인 프로젝트를 지금까지 배운 Spring, JPA, MySQL, Git을 가지고 진행해보려고 한다. 앞으로 이제 파이널 프로젝트 한 가지만 남았다. 이번 프로젝트는 PM, UIUX, FE, BE 총 15명 이상 단체로 진행하는 프로젝트이다. 잘 마무리 해보자

 

 

 

https://github.com/Yanolza-Miniproject/backend

 

GitHub - Yanolza-Miniproject/backend: 야놀자 미니 프로젝트 백엔드 리포지토리입니다.

야놀자 미니 프로젝트 백엔드 리포지토리입니다. Contribute to Yanolza-Miniproject/backend development by creating an account on GitHub.

github.com

 

 

 

[글쓰는 의도]

 

 미니프로젝트 과정에서 스프링 시큐리티를 담당했다. JWT AccessToken을 인증하는 과정에서 분명 AuthenticationException 에러가 발생하면 AuthenticationEntryPoint가 동작해서 예외를 처리하기 마련인데 아무리 찾아봐도 내가 커스텀한 AuthenticationEntryPoint 는 동작하지 않았다. 

 

 

[여러가지 해결방안]

 

 AI와 열띤 토론을 한 결과 해결책은 다음과 같았다.

 

1. 커스텀한 예외가 AuthenticationException을 상속받지 못한 경우

public class BadTokenException extends AuthenticationException

 

2. Spring Bean으로 커스텀한 AuthenticationEntryPoint를 등록하지 않은 경우

    @Bean
    JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint(){
        return new JwtAuthenticationEntryPoint();
    }

 

3. filter chain 설정에 AuthenticationEntryPoint를 등록하지 않은 경우

@Configuration
public class SecurityFilterConfig {

    private final JwtAuthenticationEntryPoint entryPoint;

 @Bean
    SecurityFilterChain http(HttpSecurity http) throws Exception {
	 http.exceptionHandling(handler -> handler.authenticationEntryPoint(entryPoint));
     return http.build();
}

 

위 3가지 모두 만족을 시켰지만 포기하던 순간

 

[어찌저찌 해결]

 

 문제는 커스텀한 필터체인을 등록하는 순서에 있었다. 나의 경우에는 jwtAuthenticationFilter을 통해 JWT 인증을 진행하고있었다. Spring Security에서 AuthenticationException이 발생한 경우 ExceptionTranslationFilter 에서 예외를 확인하고 AuthenticationEntryPoint 를 동작시킨다. 결국 ExceptionTranslationFilter 보다 jwtAuthenticationFilter가 먼저 동작을 했었기에 문제였던 것 같다. 

        http.addFilterAfter(jwtAuthenticationFilter, ExceptionTranslationFilter.class);

 

 커스텀한 Filter을 ExceptionTranslationFilter 뒤에 추가하니 잘 동작하긴 한다. 프로젝트가 바쁘다는 핑계로 시큐리티를 겉핧기로만 공부를 하고있다. filter의 동작 과정과 Security의 인증 인가 방식을 제대로 공부해야할 것 같다.

[글 쓴 의도]

 

 이번 토이 프로젝트 주제는 이전에 진행했던 동일 주제에 로그인, Open API, 회원가입 등 다양한 기능을 추가하는 방향으로 진행하기로 했다. 내가 맡은 부분은 Kaka Open API를 연결하고, 여정을 등록하는 과정에서 장소 이름을 입력하면 도로명 주소로 변환하는 기능을 구현하기로 했다. Key 값을 외부에 노출하지 않기 위해 .env 파일에 KAKAO_KEY 필드를 추가하고 application.yaml 파일에 api.key 항목을 추가해서 @Value 어노테이션으로 관리하려고 했다. 하지만 @Value 어노테이션이 잘 동작하지 않는 것을 확인했다. 이유를 알아보자

 

 

[.env 파일]

 

[application.yaml 파일]

 

 

[@Value를 적용한 필드]

 

[문제가 발생한 이유]

 

 @Value를 썼을 때, 인텔리제이가 아무 잔소리 없이 넘어갔다는 것은 매핑은 잘 되었다고 볼 수 있다. 하지만 Kakao API를 연결하는 과정에서 연결이 안된다. postman으로 직접 Kakao API에 연결도 해보고 필드에 key 값을 하드코딩을 해본 결과도 잘 동작하므로 Key 값의 오타는 아니다. 그럼 문제는 @Value가 잘 매핑되지 않았다로 결론을 냈다. 

 

@Value의 동작 순서

 여러 블로그를 참고한 결과 @Value의 동작 시점은 의존 관계 주입 시점이라는 이유였다.

 

 Spring은 의존관계 주입을 위해서 Controller, Service, Repository 등 @Component 어노테이션이 붙은 친구들을 Bean으로 등록한다. 내가 개발하는 기능은 @Component 어노테이션이 들어가지 않은 클래스에서 진행했기에 (스프링 빈으로 등록되지 않았기에) @Value 어노테이션이 깡통이었던 것이다. 

 

[해결 방법]

 

 @Value 를 사용하는 클래스에 Spring이 발견할 수 있도록 @Component가 들어간 어노테이션을 붙여주면 된다. 해당 클래스는 API 관련 기능만 담당하기에 그냥 @Component를 붙여주자.

 

 

[그래도 안돼]

 왜 안될까 @Component를 붙여도 Spring이  컴포넌트 스캔을 못하는 경우도 있을 수 있어 

 

 이건 팀원분들한테 물어보고 해결해야겠따. 일단 자고 다음에 수정해야겠어

 

 

[문제는 없을까?]

 @Component를 붙여주면 편리하지만 단점이 여러가지 있다.

 

1. Spring IoC 컨테이너에 의해 관리됨으로 테스트나 클래스를 변경할 때 불편하다.

2. 클래스를 테스트할 때 인스턴스를 직접 생성하는 것이 어렵다.

3. 고작 Key 하나만을 위해 Spring이 관리해야 하는 Bean이 늘었다.

 

대안으로는 @Configuration , @Bean을 사용해서 외부 속성으로 주입받는 방법도 있다.  -gpt-

 

 

[글을 쓰는 의도]

 야놀자 패캠 부트캠프에서 팀 단위로 하는 토이 프로젝트 마다 벨트를 꽉 매고 버스를 타고있다. 다음 프로젝트를 위해  개인 프로젝트를 만들어서 Spring JPA를 복습하려고 한다. 간단하게 프로젝트를 만들고 MySQL과 JPA를 연동하는 과정에서 애를 먹어서 나중엔 고생을 덜 하려고 글로 남기려고 한다.

 

[막힌 부분]

 먼저 Spring Init에서 간단한 의존성을 추가하고 프로젝트를 만들었다. 그리고 MySQL을 Docker-compose를 사용해서 DB를 만들었고 이 과정에서 지난 팀 프로젝트에서 썼던 외부 .env 파일을 사용해 docker.yml, .env , application.yaml을 설정했다.

 

//application.yaml

spring:
    config:
      import: optional:file:.env[.properties]

    datasource:
      url: jdbc:mysql://localhost:${LOCAL_MYSQL_PORT}/fc_sns?useSSL=false&useUnicode=true&allowPublicKeyRetrieval=true
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: ${LOCAL_MYSQL_USERNAME}
      password: ${LOCAL_MYSQL_PASSWORD}

    jpa:
      properties:
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect
      hibernate:
        ddl-auto: create

 

# docker-compose.yaml

version: '3'

services:
  mysql:
    container_name: toy2-mysql
    image: mysql:8.0
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
    environment:
      MYSQL_ROOT_USERNAME: ${LOCAL_MYSQL_USERNAME}
      MYSQL_ROOT_PASSWORD: ${LOCAL_MYSQL_PASSWORD}
      MYSQL_DATABASE: fc_sns
      TZ: Asia/Seoul
    volumes:
      - ${LOCAL_MYSQL_VOLUME_PATH}:/var/lib/mysql
    ports:
      - ${LOCAL_MYSQL_PORT}:3306

 

#.env

LOCAL_MYSQL_USERNAME =root
LOCAL_MYSQL_PASSWORD =1234
LOCAL_MYSQL_VOLUME_PATH =C:\toytoy
LOCAL_MYSQL_PORT =3306

 

 

위와 같이 설정 파일을 지난 프로젝트에서 사용했던 그대로 들고와서 설정했는데 이리저리 다 만져봐도 DB에 접속하는 URL 관련 에러가 나오곤 했다.

 

[해결]

 .env 파일을 사용하는 과정에서 LOCAL_MYSQL_USERNAME = root 에서 '=' 기호 앞에 띄어쓰기(' ')를 사용해서 JPA가 인식을 하지 못했던 이유였다.

 

Title : 여행 여정을 기록과 관리하는 SNS 서비스 2단계

 2단계 토이프로젝트는 한 달 전 팀원들과 동일하게, 비슷한 주제로 Spring을 가지고 진행했다. 1단계 서비스와 비슷한 요구사항을 가지고 특징은 API를 설계하고 개발하는 것이 중요하다고 생각했다. 워낙 팀원분들이 잘 하시는 분들이여서.. 이번에도 배운다는 마인드로 임했다. 1단계에서는 여정 부분을 개발했다. 2단계에서도 역할을 나누기 위해 랜덤으로 역할을 정하고 나는 여정 부분을 맡게 되었다.

 

 

 

개발 일정

 

[토이프로젝트 개발 일정 (5일)]

 

 

 개발 일정은 위 사진과 같이 동일하게 진행했다. 첫 날에는 API 설계 및 ERD 설계를 진행하고 팀원 역할을 배정했다.

나머지 일정도 위 계획표와 비슷하게 동일했다.

 

API 설계 (여행, 여정)

 

[API 설계, 여행]

 

[API 설계, 여정]

 

 

 API 설계는 위에 사진과 같이 진행했다. 1단계와 비슷한 내용이기에 빠르게 작성했고 여기서 배운 것은 URL에 복수를 사용하기, 계층형 구조를 사용하기를 배웠다.

 

 

ERD, 클래스 다이어그램

 

[ERD 다이어그램]

 

 ERD 다이어그램은 위 사진과 같이 설정했다. 이번 프로젝트에서는 JPA의 Embedded 를 사용했다. 그렇기에 여정에 필드가 많아지기는 했지만 요구사항에는 제한되지 않았다. 

 

 

[클래스 다이어그램]

 

 클래스 다이어그램에서의 특징은 여정을 구성하는 체류 , 이동, 숙소 정보 모두 시간과 장소를 가지는 특징을 가졌다. 그렇기에 팀원분들끼리 상의한 결과 PlaceInfo(장소 명을 표기), DataTimeScheduleInfo(시작, 종료 시간을 표기) 를 사용하기로 했다. 이유는 장소의 경우 현재 요구사항은 장소명(String)만 표기하고 있다. 추후에 만약에 장소에 관한 필드(장소 명, 도로명주소, 우편번호 ..)가 추가될 경우 편리하게 유리하게 수정하기 위해서 장소만 관리하는, 장소를 책임지는 하나의 클래스를 여정클래스에서 분리하여 사용했다.

 

Git, Code 컨벤션

 

 

[Git 브랜치 전략]

 

[Commit 컨벤션]

 

 이번 프로젝트는 깃, 커밋, 코드 컨벤션을 사전에 정하고 프로젝트 내에서의 코드, 구조를 최대한 통일하려고 노력했다.

 git flow를 사용했으며 커밋 메세지도 쉽게 확인할 수 있게 컨벤션을 사용했다. 추가로 git issue 주도 개발을 진행했다. 사용할 기능에 대한 Issue를 미리 만들고 개발을 진행하는, 커밋 메세지에 git Issue를 링크하는 재밌었던 경험을 했다.

 

개발 사항 및 프로젝트 느낀점

 

 이번 프로젝트에서 여정 부분을 개발했다. 기본적으로 사용한 기능들은 ResponseEntity로 응답 값을 가공해서 반환했으며 DDD 구조, Controller, Service, Repository 3계층 구조를 사용했다. 추가적으로 모든 조가 JPA를 프로젝트에 적용했는데 나는 아직 공부를 게으르게 해서 JPA를 잘 사용하지 못했다. 다음 프로젝트 기간 전까지 JPA와 시큐리티를 완벽하게 공부할 필요를 뼈저리게 느꼈다. 이번 프로젝트 역시 버스를 탔다. 팀원분들에게 눈치가 많이 보이지만 다들 엄청 잘하시는 분들이여서 정말 나 없이도 프로젝트가 돌아갈 정도다. 얼른 열심히해서 팀원분들 처럼 실력을 키우고 나도 프로젝트를 캐리하고싶다. 

 

 

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

https://school.programmers.co.kr/learn/courses/30/lessons/150370

 

 

import java.util.*;
import java.time.*;
class Solution {
    
    public LocalDate func(int year, int month, int day, int terms){

        LocalDate days = Year.of(year).atMonth(month).atDay(day);
        
        return days.plusMonths(terms);
    }
    
    public int[] solution(String today, String[] terms, String[] privacies) {
        int[] answer = {};
        HashMap<String, Integer> hm = new HashMap();
        String[] targetday = today.split("\\.");
        
        LocalDate A = func(Integer.parseInt(targetday[0]), Integer.parseInt(targetday[1]), Integer.parseInt(targetday[2]), 0);
        
        for(int i  = 0; i<terms.length; i++){
            String[] tmp = terms[i].split(" ");
            hm.put(tmp[0], Integer.parseInt(tmp[1]));
        }
        
        for(int i = 0; i<privacies.length; i++){
            
            String[] str = privacies[i].split(" ");
            String type = str[1];
            String[] days = str[0].split("\\.");          
            int year = Integer.parseInt(days[0]);
            int month = Integer.parseInt(days[1]);
            int day = Integer.parseInt(days[2]);
            
            LocalDate B = func(year, month, day, hm.get(type));
            System.out.println(A);
            System.out.println(B);
            
            
            if(A.isAfter(B)){
                System.out.println(i+1);
            }
        }
        
        return answer;
    }
}

처음 생각한 방법 : Split을 사용해서 파싱하고, 각 파싱한 변수로 LocalDate 객체를 만들어서 isAfter을 사용해서 문제를 해결하려고함

Split 파싱할 때, " . " dot을 기준으로 하려했는데 그냥 점만 찍으면 안됨..

 

코드를 실행해봤는데 어떤건 맞고 어떤건 틀림, 아마 문제에서 한 달이 28으로 고정이라고 해서 LocalDate를 사용하면 안되나봐요 다시 풀어보자

 

Title : 여행 여정을 기록과 관리하는 SNS 서비스 1단계

 

 이번 토이프로젝트는 조를 새로 배정 받아서 5인 1조로 과제를 수행했다. 첫 조원분들과 만나는 자리에서 회의를 간단하게 진행했는데 다 프로젝트 경험이 여러 번 있으시고, 관련 지식도 많은 팀원 분들을 만났다. 토이프로젝트 진행은 화면에 출력하는 view 1명, 여행관련 2명, 여정관련 2명으로 나눠서 진행했고 나는 여정 파트를 맡았다. 개발 일정과 프로젝트 기획은 아래와 같다.

 

 

[개발 일정]

 

 

[팀 프로젝트 구상도]

 

 제안서에는 여행속에 여정이 배열로 존재하는 구성이었는데, 우리 조는 여행 파일 따로, 여정 파일 따로 관리하기로 하였고, 여정을 추가, 읽기 위해서는 여행 파일의 인덱스가 필요하다는 특징이 있다. 그렇게 구조를 설계하고 각자 개발에 들어갔다. 나는 이번이 여럿이서 하는 첫 프로젝트여서 협업이 어색했다. 다들 나보다 잘 하셔서 설계하는 내용을 경청하고 배웠던 시간이었다. 또 같은 여정을 개발하는 팀원분께서 내가 이해되지 않은 부분을 따로 시간 내서 설명해주시고, git 관련 이슈도 잘 해결해주셨다.

 

[토이프로젝트 순서도]

 

여정에서의 예외처리

 기억에 남는 부분은 예외처리를 한 부분이다. 첫 토이프로젝트에서는 여행, 여정 파일을 Json, Csv로 관리하는 설정인데 이 과정에서 발생할 수 있는 예외를 다음과 같이 정의했다.

 

1. JSON, CSV 파일을 읽을 때, 각 형식에 알맞지 않은 경우

2. 여정을 추가할 때, 여정을 추가하려는 여행 파일이 존재하지 않는 경우

3. 여정을 추가할 때, 여행 일정에 알맞지 않은 여정을 추가하려는 경우

 

 위 3개의 예외를 핸들링하면서 예외에는 체크 예외와 언체크 예외가 존재한다는 것을 알게되었다. FILE IO EXCEPTION을 캐치하고 평소와 같이 throw new 키워드를 작성해서 RunTime Exception을 던졌었는데, CSV Parse 관련 Exception은 File IO를 상속받았기에 반드시 처리를 해줘야만 했다. 나는 뭣도 모르고 그냥 한꺼번에 RunTime Exception을 상속받아서 처리하면 깔끔하게 처리가 되길래 하고 나서도 찜찜했기에 그룹스터디 시간 멘토링 때 멘토님에게 여쭤봤다.

 

1. 예외처리는 예민할 정도로 고려해야 한다.

2. File IO 관련 예외를 RunTime Exception으로 처리하는 방법도 하나의 방법이다.

3. 위와 같은 예외의 경우 백엔드 개발자 입장에선 에러를 내는게 최선이다.

 

라는 답을 들었다. 1번의 경우 이런 것 까지 예외를 처리해? 라는 말을 들을 정도로 단단묵직하게 예외를 고려해야 할 것이고, 가볍게 생각해선 안되는 느낌이다. 2번 3번의 경우 File IO는 체크예외라서 이를 상속받는 모든 예외는 컴파일러에서 한 번 걸러지게 되는데 개발자 입장에서는 프로그램이 실행되는 도중에 오류의 원인인  .json, .csv 파일을 당장 열어서 해결할 것 도 아니므로 RunTime Exception으로 처리하는 방법도 하나의 방법이라고 알려주셨다. Spring 에서는 ExceptionHandler를 사용하고, 값 검증에는 Validation을 사용하기에 그냥 예외에는 어떤 종류가 있는지도 모르고 무작정 던지기만 했었는데 이번 기회에 가볍게라도 알게 되었다.