아직 해결중입니다. 버그가 쉽게 해결되지 않아서 스트레스가 밀려오지만 또 면접에서 할말 생겼다는 생각이 들어서 긍정적으로 생각하기로 했습니다. 집에서 공부하니까 집중이 잘 안되는 거 같아 나가려다 영하 17도를 보고 다시 담요를 여몄습니다. 집 세탁기가 얼었습니다. 다행히 보일러는 아직 살아있네요. 

 

발생한 버그에 대한 로그는 아래와 같습니다.

Connection leak detection triggered

 Connection leak detection triggered for com.mysql.cj.jdbc.ConnectionImpl@

HikariPool에 대해서 나오고 있어서 Hikari가 문젠가 하고 먼저 들여다 보았습니다.

 

HikariCP DeadLock

잠재적인 원인에 대해서 공부를 하기로 했습니다. 

 

HikariCP Dead lock에서 벗어나기 (이론편) | 우아한형제들 기술블로그

{{item.name}} 안녕하세요! 공통시스템개발팀에서 메세지 플랫폼 개발을 하고 있는 이재훈입니다. 메세지 플랫폼 운영 장애를 바탕으로 HikariCP에서 Dead lock이 발생할 수 있는 case와 Dead lock을 회피할

techblog.woowahan.com

읽어보니 저희 오류랑은 조금은 다릅니다.

 

 

 

제가 다시 눈여겨본 로그는 다음입니다:

Async request timed out

 

무시해도 된다는 블로그 글을 찾긴 했습니다만...(https://jsonobject.tistory.com/558)  궁금해서 찾아봤습니다.

 

디버깅은 계속됩니다....

'TIL' 카테고리의 다른 글

TIL @EventListener 알림 기능 강한 결합 제거 230127  (0) 2023.01.28
TIL @Async 비동기 동기 230126  (0) 2023.01.27
TIL SSE 에러 230124  (0) 2023.01.25
TIL 설연휴 월요일 230123  (0) 2023.01.23
TIL 설날 2023 230122  (0) 2023.01.23

알림관련해서 성공한 줄 알았는데 아니었습니다. FE와 연결 시 발생한 SSE 관련 에러는 다음과 같습니다. 

 

1. subscribe  성공 후, 알림 2~3번 수신하면 서버가 멈추는 현상 -> DB Connection Pool이 다 차서 connection leack 이 발생

2. open-in-view 를 false로 설정하면 된다길래 (https://tecoble.techcourse.co.kr/post/2022-10-11-server-sent-events/) 설정했더니, transaction에 문제가 생겨 다른 service들 기능이 안되고 알림만 되는 현상

 

 

일단 오류가 발생한 것을 확인했습니다.

 

또 FE로부터 읽지않은알림 갯수를 보내달라는 요청을 받았는데 두 가지 안이 있습니다. 

FE에서 제안한 방법은

 

- 이벤트가 발생해서 알림을 보내줄 때 함께 읽지않은알림 갯수를 보내주기

 

입니다.

 

저의 생각은

 

- API를 새로 파서 unread notification count를 따로 보내주는 겁니다. 

 

 

여러 깃헙을 찾아보고 구글링해보면서 방법을 찾고 있습니다.

 

SSE가 자료도 많아서 간단할 줄 알았는데 쉽지 않네요. 

'TIL' 카테고리의 다른 글

TIL @Async 비동기 동기 230126  (0) 2023.01.27
TIL SSE 에러 트러블슈팅 230125  (0) 2023.01.26
TIL 설연휴 월요일 230123  (0) 2023.01.23
TIL 설날 2023 230122  (0) 2023.01.23
TIL 최종 프로젝트 중간 발표 230121  (0) 2023.01.23

오늘은 설연휴 첫날입니다. 게더에 접속했는데 생각보다 사람들이 많이 없어서 놀랐어요. 공부를 좀 해야겠습니다. 이제 정말 3주도 안남았거든요. 하차를 많이 한다고 했는데 하차를 할 이유는 이제 없어졌습니다. 이제 이 부트캠프에 투자한 돈과 시간이 가치가 있었는지를 검증할 일만 남았네요. 

 

내년 설연휴에는 어떤 모습으로 TIL을 쓰고 있을지 궁금해지네요. 인생이 5G로 달리고 있는 느낌입니다.

아침부터 차례지내고 친척들과 담소를 나누니 피곤해서 뻗었습니다. 저녁에는 오랜만에 친구들과의 약속이 있어서 나갔다 왔습니다. 대학교친구들인데 어느새 저희가 만난지도 10년이 다되어 간다고 하며 세월이 너무 빠르다고 했습니다. 제가 부트캠프 한다고 작년 가을에 말했었는데 그것도 이제 3주도 안남았습니다. 

 

근데 재밌는건 다른 친구들도 진로 변경에 대한 고민을 하고 있다는 거였습니다. 지금 나이가 과도기인거 같아요. 다들 뭐든 잘 할 수 있어서 더욱 더 고민이 되는 것 같습니다. 하나만 잘하는 게 아니라 여러개를 잘하는데 여러개에 흥미가 있어서 여러가지를 도전할 수 있는 상황인거에요. 지금 저희 세대는 평균 수명이 100세가 넘어가서 앞으로 최소 80년은 더 살아야하고 직업을 3~4개 가질 거라고 합니다. 저는 이미 하나는 클리어 했고 2개째 입니다. 고용되는 직업으로서는 그렇습니다. 

 

웹개발자는 전세계에 내가 만든 것을 선보일 수 있다는 점에서 더 재미가 있습니다. 디지털 세상에 내 영토를 만드는 것이라고 생각하면 개발자는 매력적인 직업입니다.

 

 

 

중간발표하면서 느낀건 이 하나의 프로젝트로 면접할 때 할말은 많겠다 입니다.

예상질문 잘 정리해서 답변 잘 할 수 있도록 준비해야겠습니다.

드디어 내일이 중간 발표날입니다. 조금은 해이해진 요즘인데 프론트로부터 알림 구현에 대해 수정 요청이 들어왔습니다. 제가 요청받은 사항은 아래와 같습니다.

 

  1. redirect 되는 페이지의 정보를 게시물 타입, 게시물 아이디로 보내주기
  2. 알림에 알림을 발신하는 멤버의 정보(닉네임, 프로필사진)을 추가
  3. 알림 목록 조회시 최신순으로 조회

 

1, 2번을 하고 실행을 눌렀는데 어제와 같은 에러가 발생했습니다. IP 주소를 확인하니 그새 또 변경이 되었더라고요. 제가 무슨 해커도 아니고 맨날 IP 주소가 변경이 되네요. 고정 IP 신청을 하면 된다는데 일단은 버텨봅니다.

 

 

비관적 락 pessimistic Lock

@Transactional
@Lock(LockModeType.PESSIMISTIC_WRITE)
@QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value ="3000")})
Optional<PostLike> findByPostLikedIdAndMemberId(Long postLikedId, Long memberId);

이틀 전부터 있던 버그는 위와 같이 비관적 락을 걸어줘서 바꿨습니다. 

 

TIL 동시성 문제? 나의 첫 버그 230118

동시성 문제로 추정되는 오류가 발생했다. 게시글 1에서는 좋아요가 잘 반영이 되는데 게시글 2부터 안된다. 이럴수가. 뭐가 문제냐. 여러가지 자료를 찾아보고 있는데 이런걸 바로 버그라고 하

pizzathedeveloper.tistory.com

비관적 락(Pessimistic Lock)을 사용하는 이유는 아래와 같습니다.

 

1. 트랜잭션이 충돌한다고 가정하고 락을 사용

2. DBMS의 락 기능을 사용

3. 데이터 수정 시 즉시 트랜잭션 충돌 여부를 확인 가능

 

비관적 락에는 두가지 종류가 있습니다:

  • Exclusive Lock은 데이터를 변경하고자 할 때 사용되며, 트랜잭션이 완료될 때까지 유지되어 해당 Lock이 해제될 때까지 다른 트랜잭션은 해당 데이터에 읽기를 포함하여 접근을 할 수 없습니다.
  • Shared Lock은 다른 사용자가 동시에 데이터를 읽을 수는 있지만 Write는 할 수 없습니다.

PESSIMISTIC_WRITE Exclusive Lock을 걸고, 데이터를 읽거나, 업데이트하거나, 삭제하는 것을 막습니다.

 

JPA는 PESSIMISTIC_WRITE 이외에도 

 

  • PESSIMISTIC_READ - Shared Lock을 걸고 데이터가 업데이트 되거나 삭제되지 않도록 한다
  • PESSIMISTIC_FORCE_INCREMENT - PESSIMISTIC_WRITE와 유사하고 ENTITY의 버전 속성을 추가로 증가시킨다.

 

EXCEPTION의 경우에는 PersistenceException(락을 가져오는데 실패하면 예외 발생), LockTimeoutException(락을 기다리다가 설정해놓은 wait time을 지났을 때 예외 발생) 이 있습니다.

 

저는 좋아요 실행 시 충돌이 발생한 것을 이미 봤기 때문에 충돌이 발생한다는 것을 가정하는 비관적 락을 걸었습니다. Lock이 길어지면 Latency가 길어지기 때문에, @QueryHints를 사용하여 Lock에 3초 타임 아웃을 적용했습니다.

 

 

 

결과는?

잘 됩니다.

좋아요를 여러번 연속해서 처리할 때 true 값만 반환이 되던(아마 충돌이 일어나서 실행이 되지 못했겠던) 현상이 사라지고 정상적으로 잘 작동이 됩니다.

 

 

 

프론트 요청 처리하기

새로운 칼럼을 추가하려고 하니 이미 DB가 서버에 올라가 있어서 다 날리고 새로 올리는 작업을 여러번 반복해야했습니다. 저만 칼럼을 수정하고 추가하고 있던게 아니었거든요.. API 명세가 처음이랑은 전혀 다른 모습이 되었습니다. 내일이 중간 발표인데 중간 발표가 끝나면 한번 정리하는 시간을 가져야 할 것 같습니다.

 

이제는 CRUD 수정하는 정도는 금방 해버립니다. 실제로 위의 1,2,3, 요청은 2 시간도 안걸려서 해결해버렸답니다. JPA가 정말 편해요.. 스프링은 사랑입니다😘

 

 

아래와 같이 SQL Error 가 발생했습니다.

SQL 에러

기존에 있던 Column 명을 변경해서 에러가 생긴 줄 알았는데 SQL에 등록해놓았던 ip 주소가 변경되어서 그런거였습니다.

ip 주소는 고정 ip를 신청하지 않는 이상 변경 될 수 있다고 하네요. 

동시성 문제로 추정되는 오류가 발생했다.

게시글 1에서는 좋아요가 잘 반영이 되는데 게시글 2부터 안된다.

 

이럴수가.

 

뭐가 문제냐.

 

여러가지 자료를 찾아보고 있는데

이런걸 바로 버그라고 하나보다.

 

제대로 된 버그는 처음이다.

보통 에러가 뜨는데 말이다...

에러가 안뜨는데 작동이 잘 안되는 건 처음이다.....

 

로그인을 하지 않은 유저가 게시글을 상세 조회 했을 때, 좋아요 여부와 팔로우 여부는 false 값으로 처리가 되는 로직을 구현해야했습니다. 그리고 member 값이 null 로 들어가도 nullpointerException이 발생하지 않게 예외 처리를 해줘야했습니다.

 

게시글과 콜라보 조회 시에 발생하는 문제였습니다. 

 

member 값을 받아와야 해당 유저의 정보를 확인해서 팔로우를 했는지, 좋아요를 했는지를 알 수 있는데 로그인을 하지 않은 유저라면 member 값을 불러 올 수가 없어서 null로 뜨게 되서 NullPointerException이 발생합니다. 아래와 같이 NullPointerException을 처리했습니다.

Controller

@GetMapping("/details/{postid}")
public ResponseEntity<SuccessResponse<Object>> infoPost(@PathVariable Long postid, @AuthenticationPrincipal CustomUserDetails userDetails){
    Member member = null;
    try {
        member = userDetails.getMember();
    }catch (NullPointerException e){
        log.info("비로그인 사용자 접근 : infoPost");
    }
    return SuccessResponse.toResponseEntity(INFO_POST,postService.infoPost(postid, member));
}
  • @Slf4j 을 사용해서 log를 찍었습니다.
  • try-catch문을 사용하여서 NullPointerException 이 발생하면 log를 띄워주었습니다.
  • member 값은 null로 초기화 했습니다.

 

Service

@Transactional
public InfoPostDto infoPost(Long postid, Member member) {
    Post post = postRepository.findById(postid).orElseThrow(
            () -> new NotFoundException(POST, SERVICE,POST_NOT_FOUND)
    );

    post.viewCount();

    postRepository.save(post);

    Boolean isLiked = false;

    if(member!=null){
    Member postMember = memberRepository.findByNickname(post.getNickname()).orElseThrow(
            () -> new NotFoundException(POST, SERVICE, MEMBER_NOT_FOUND)
    );

    PostLikeCompositeKey postLikeCompositeKey
            = new PostLikeCompositeKey(member.getId(), postMember.getId() );
    if (postLikeRepository.findById(postLikeCompositeKey).isPresent()){
        isLiked = true;
    }
    }

    return new InfoPostDto(post, isLiked);
}
  • if문을 사용해서 member가 null이 아닌 경우, 좋아요 여부를 반환합니다.
  • null 인 경우에는 초기값인 false를 반환합니다.

 

 


이번주 토요일에 중간 발표가 있습니다. 여러모로 착착 진행되고 있습니다. 프론트와 소통해서 자잘한 에러를 수정하거나 api를 수정하고 있습니다.

자꾸 바뀝니다.

 

프론트랑 통신하면서 API 명세도 계속 바뀝니다.

웹개발자가 되겠다고한지 벌써 5개월차입니다. 친한 지인이 퇴사하고 부트캠프 할거라고 해서 이미 고연봉자인 지인이 한다니 호기심이 생겼던 게 지난 9월이었습니다. 가족이 새로 하는 사업 때문에 백수였던 제가 도와드리고 있었는데 '웹사이트를 만들어봐라' 하셔서 돈주고할려다가 너무 비싸서 제가 만들기 시작하고 있었던 것도 한 몫 했습니다. 웹개발 툴(아임웹)을 쓰고 있었는데 간단한 html 코드 정도 써보고 있던 수준이었습니다.  부트캠프의 존재도 모르고 있었는데 어떻게 벌써 이렇게 한달밖에 안남은 시점이 되었네요. 

TIL

이제는 CRUD 정도는 척척하게 되었습니다. 처음에는 "왜 다 안알려주고 이렇게 불친절한 부트캠프를 봤나!" 하고 살짝은 불만이었던 부분들이 어느정도 해소가 되었습니다. 하다보면 자연히 필요에 의해 찾아볼 수 밖에 없습니다. 그래도 저는 여전히 제가 하고 있는 항해99를 추천하냐고 묻는다면 공부를 아주 열심히 해본 적 있는 분들만, 어느정도 이과적 공부 머리가 있는 분들에게 추천합니다.  뭐... 12시간동안 앉아서 공부할 의지가 있다는 거 자체가 남들과는 다른 의지와 공부 습관을 가진 사람들이라고 할 수는 있겠다는 생각이 듭니다. 부트캠프 신청자체가 진입장벽이라고 생각하면 괜찮을 수도 있겠네요. 저는 원래 공부만 하던 사람이라 괜찮았습니다. 스스로 찾아서 하는 개발자가 되는게 중요하다고 했는데 저는 원래 하던 일도 그래서 괜찮았습니다. 

 

친절하고 열심히하는 동료들을 만나는 것도 큰 행운으로 작용해서 제가 부트캠프에 열정적으로 임하게 되는 요인중에 하나입니다. 저는 아직까지 항해99의 동료들에게 불만이 있었던 적이 없습니다. 다들 너무 감사하고 제가 작년에 있던 불운들은 이분들을 만나기 위해 행운을 아껴놓았던 거라고 생각할 정도입니다. 최종 프로젝트는 6주동안 함께 하는 사람들인데 다들 열정적이고 저보다 코딩 배운지 오래 되신 분들이 대부분이라 배울점이 너무 많은데 친절하시기까지 하고, 새로 들어온 디자이너님도 열정 있으시고 실력도 뛰어나서 또 내게 이런 행운이 왔구나하는 마음에 감사했습니다. 저 또한 이분들에게 좋은 동료가 되고 싶습니다.

 

남은 한달동안은 최종 프로젝트를 잘 마무리하고 이력서도 쓰고 코딩 테스트 준비도 하고 면접준비도 하면 금방 지나갈 것 같습니다. 지금까지 해왔던 것처럼 앞으로도 꾸준히 무소의 뿔처럼 나아가겠습니다. 

 

 

멘토링 시간이 있었습니다. 

 

에러 처리를 할 때, 어디서 누가 무얼하다가 라는 정보를 넣으면 좋겠다는 조언이 있었습니다.

예를 들어 쇼핑몰에서 회원이 물건을 사다가 오류가 났으면, member Id와 그 상품의 id, 그리고 그 시간을 찍어줍니다.

또 stack trace를 넣어줍니다.

 

에러가 뜨면 쫙 뜨는 그 긴 문장들이 stack trace라는 것을 처음알았습니다. 배울게 많습니다. 

 

What is a Stack Trace? - Definition from Techopedia

This definition explains the meaning of Stack Trace and why it matters.

www.techopedia.com

영어를 할 줄 아는 개발자가 장점이 많을 거라는 생각이 드네요. 에러 메세지가 한글로 뜨면 참 좋을텐데 말이죠.

아 물론 저는 잘합니다😎

 

Stacktrace

stack trace는 프로그램 서브루틴에 대한 정보를 제공하는 보고서입니다. 일반적으로 stack trace을 통해 소프트웨어 엔지니어가 문제가 있는 위치 또는 실행 중에 다양한 서브루틴이 함께 작동하는 방식을 파악하는 데 도움이 되는 특정 종류의 디버깅에 사용됩니다.

 

그리고 저의 일주일 회고를 이야기하는데 이번에 구글링을 하면서 블로그 자료들 중에 정확하지 않은 것도 많고 구린것도 많아서 정보를 보는 눈을 키우고 싶다고 말씀드렸더니 공식문서를 읽으라고 하셨습니다. 부트캠프에서 학생들에게 강제하다보니 기술블로그도 우후죽순 생겨나서 그렇다고 하셨습니다. 앗 근데 저도 괜히 반성하게 되서 기술 관련해서 리뷰할 때는 좀 더 정확하게 써야겠다고 다짐했습니다.

 

좋아요를 복합키 composite key 를 사용해서 구현했습니다. 

 

Controller

@PostMapping("/api/post/like/{postid}")
public ResponseEntity<SuccessResponse<ResponsePostLikeDto>> postLike(@PathVariable Long postid, @AuthenticationPrincipal CustomUserDetails userDetails){

    return SuccessResponse.toResponseEntity(POST_LIKE, postLikeService.postLike(postid, userDetails.getMember()));
}
  • 좋아요하려는 post의 Id와 멤버 정보를 가져옵니다.

 

Service

public ResponsePostLikeDto postLike(Long postId, Member member){
    Post postliked = postRepository.findById(postId)
            .orElseThrow(()-> new NotFoundException(LIKE, SERVICE, POST_NOT_FOUND)
            );
    PostLikeCompositeKey postLikeCompositeKey
            = new PostLikeCompositeKey(member.getId(), postliked.getId());
    boolean likecheck;

    if(postLikeRepository.findById(postLikeCompositeKey).isPresent()){
        postLikeRepository.deleteById(postLikeCompositeKey);
        postliked.disLike();
        postRepository.save(postliked);
        likecheck = false;

        return new ResponsePostLikeDto(likecheck, postliked.getLikeCount());
    }

    postLikeRepository.save(new PostLike(postLikeCompositeKey, member,postliked));
    likecheck=true;
    postliked.likeCount();
    postRepository.save(postliked);

    return new ResponsePostLikeDto(likecheck, postliked.getLikeCount());

}
  • 좋아요한 id 가 있으면 취소하고 좋아요 수를 1 감소시키고, 없으면 좋아요 아이디를 생성하고 좋아요 수를 올려줍니다.
  • Boolean 타입 likecheck을 반환해서 프론트에 보내줍니다. 프론트에서 확인하고 좋아요 아이콘 상태를 변경하는 데 사용합니다.
  • post에 likeCount 를 위한 메서드를 넣어줍니다.
public void likeCount(){
    this.likeCount++;
}

public void disLike(){
    this.likeCount--;
}

 

Composite Key

@Embeddable
@NoArgsConstructor
public class PostLikeCompositeKey implements Serializable {

    @Column(nullable = false)
    private Long memberId;

    @Column(nullable = false)
    private Long postLikedId;

    public PostLikeCompositeKey(Long memberId, Long postLikedId){
        this.memberId = memberId;
        this.postLikedId = postLikedId;
    }
}
  • 복합키는 memberId와 postLikedId를 가집니다. 좋아요한 postId 입니다.

 

PostLike

@Entity
@NoArgsConstructor
@Getter
public class PostLike {

    @EmbeddedId
    private PostLikeCompositeKey id;

    @MapsId("memberId")
    @ManyToOne(fetch = FetchType.LAZY)
    private Member member;

    @MapsId("postLikedId")
    @ManyToOne(fetch = FetchType.LAZY)
    private Post postLiked;

    public PostLike(PostLikeCompositeKey id, Member member, Post postLiked){
        this.id = id;
        this.member = member;
        this.postLiked = postLiked;
    }
}
  • postLike 엔티트를 생성합니다.
  • 복합키 composite key를 사용했기 때문에 @EmbeddeId, @MapsId 어노테이션을 사용합니다.

 

Respository

@Component
public interface PostLikeRepository {
    PostLike save(PostLike postLike);
    @Transactional
    void deleteById(PostLikeCompositeKey postLikeCompositeKey);
    Optional<PostLike> findById(PostLikeCompositeKey postLikeCompositeKey);
}
  • delete 쿼리를 사용하기 때문에 @Transactional를 추가해줍니다. 안하면 에러 뜹니다....(저도 알고싶지 않...)
  • Optional로 findById 메소드를 구현한 것은 찾는 값이 null 일 때 에러를 방지하기 위해서 입니다.

 


좋아요 기능 구현을 위해 복합키를 처음 사용해봤는데 생각보다 어렵지는 않고 단순했습니다. 그냥 안써도 될 것 같다는 느낌이 드는데 공부가 좀 더 필요한 것 같습니다.

 

 

 

 

 

오늘은 알림기능을 위해서 먼저 구현해야하는 좋아요를 복합키를 사용해서 구현해보려고 합니다. 게시글 좋아요, 댓글 좋아요가 필요한데 복합키를 써보는 경험을 다 같이 해보기 위해서 저는 게시글 좋아요 다른 팀원분이 댓글 좋아요를 맡았습니다. 복합키 자체가 자료가 잘 안나오네요. 조장님이 복합키 사용해보라고 했는데 한번 도전해봅니다. 일단 뭔지 알아야겠지요.

 

Composite Key 복합키

복합키는 Composite Key로 2개 이상의 column을 프라이머리 키로 가지고 있습니다. 복합키는 @IdClass와 @EmbeddedId 어노테이션을 사용해서 정의할 수 있습니다. 복합키는 다음과 같은 규칙을 가지고 있습니다.

  1. composite key는 반드시 public 이어야 한다.
  2. 반드시 @NoArgsConstructor 를 가진다.
  3. equals() 와 hasCode() 메서드를 정의해야한다.
  4. 반드시 Serialized 되어야 한다.

 

 

https://www.baeldung.com/jpa-composite-primary-keys

 

SSE 방식으로 실시간 알림을 구현하는 이유는 지난 포스팅에서 확인하실 수 있습니다.

 

TIL 알람 기능 구현 SSE(Server-Sent-Events) 230110

오늘부터 MVP 2차를 개발하기 시작했습니다. 알람 기능 구현이 제가 맡은 부분입니다. 프론트엔드가 2명밖에 없어서 알람까지 할 수 있을지는 모르겠지만 일단은 만들고 생각하기로 했습니다. 프

pizzathedeveloper.tistory.com

 

 

우선 SSE를 연결하기위해서 컨트롤러부터 구현했습니다.

처음에는 Alarm이라는 도메인명을 사용하려다가 알람! 보다는 알림~ 이 좋을 거 같아서 Notification으로 변경했습니다.

 

NotificationController - SSE Subscribe 응답하기

@RestController
@RequiredArgsConstructor
public class NotificationController {

    private final NotificationService notificationService;

    @Tag(name = "SSE")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "2000", description = "SSE 연결 성공"),
            @ApiResponse(responseCode = "5000", description = "SSE 연결 실패")
    })
    @Operation(summary = "SSE 연결")
    @GetMapping(value="/api/subscribe", produces = "text/event-stream")
    public SseEmitter subscribe(@AuthenticationPrincipal CustomUserDetails userDetails, @RequestHeader(value="Last-Event-ID", required = false, defaultValue = "") String lastEventId ){

        return notificationService.subscribe(userDetails.getMember().getId(), lastEventId);
    }
  • @GetMapping 어노테이션에 아래와 같이 입력해줘야 합니다.
produces = "text/event-stream"
  • SSE 통신을 위한 "text/event-stream"이 표준 MediaType입니다.
  • MemberId 값과 "Last-Event-ID"를 받아옵니다.
  • Last-Event-ID는 SSE 연결이 끊어졌을 경우, 클라이언트가 수신한 마지막 데이터의 id 값을 의미합니다.
  • 항상 있는 것이 아니기 때문에 required = false 로 설정했습니다.

 

NotificationService - SSE 연결

@Service
@RequiredArgsConstructor
public class NotificationService {
    private final EmitterRepository emitterRepository = new EmitterRepositoryImpl();
    private final NotificationRepository notificationRepository;

    private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60;

    public SseEmitter subscribe(Long memberId, String lastEventId) {
        String emitterId = memberId + "_" + System.currentTimeMillis();
        SseEmitter emitter = emitterRepository.save(emitterId, new SseEmitter(DEFAULT_TIMEOUT));

        emitter.onCompletion(() -> emitterRepository.deleteById(emitterId));
        emitter.onTimeout(() -> emitterRepository.deleteById(emitterId));

        sendToClient(emitter, emitterId, "EventStream Created. [memberId=" + memberId + "]");

        if (!lastEventId.isEmpty()) {
            Map<String, Object> events = emitterRepository.findAllEventCacheStartWithByMemberId(String.valueOf(memberId));
            events.entrySet().stream()
                    .filter(entry -> lastEventId.compareTo(entry.getKey()) < 0)
                    .forEach(entry -> sendToClient(emitter, entry.getKey(), entry.getValue()));
        }

        return emitter;
    }
  • DEFAULT_TIMEOUT을 1시간으로 설정해줬습니다. SSE 연결은 1시간동안 지속됩니다.
  • SseEmitter 클래스는 SpringFramework에서 버전 4.2 부터 제공합니다.
  • emitter는 발신기라는 뜻을 가지고 있습니다. 
  • String emitterId = memberId_System.currentImeMillis(); 로 한 이유는 Last-Event-ID와 관련이 있습니다.
    • Last-Event-ID는 클라이언트가 마지막으로 수신한 데이터의 Id값을 의미합니다. 그러나 Id 값만을 사용한다면 언제 데이터가 보내졌는지, 유실 되었는지 알 수가 없기 때문에 시간을 emitterId에 붙여두면 데이터가 유실된 시점을 알 수 있으므로 저장된 Key값 비교를 통해 유실된 데이터만 재전송 할 수 있습니다.

 

SseEmitter emitter = emitterRepository.save(emitterId, new SseEmitter(DEFAULT_TIMEOUT));

emitter.onCompletion(() -> emitterRepository.deleteById(emitterId));
emitter.onTimeout(() -> emitterRepository.deleteById(emitterId));
  • SSE 연결을 위해서 SseEmitter 객체를 만들어 반환해야합니다.
    • 유효시간 DEFAULT_TIMEOUT을 넣어줍니다.
    • 시간이 지나면 자동으로 클라이언트에서 재연결 요청을 보냅니다.
    • emitterId도 함께 저장해줍니다.
  • 시간이 초과하거나 비동기요청이 정상동작이 안되면 저장한 SseEmitter를 삭제합니다.

 

sendToClient(emitter, emitterId, "EventStream Created. [memberId=" + memberId + "]");
  • Sse 연결이 이뤄진 후, 데이터가 하나도 전송되지 않았는데 SseEmitter의 유효시간이 끝나면 503 에러가 발생한다고 합니다. 따라서, 최초 연결 시 더미 데이터를 보내줍니다.

 

if (!lastEventId.isEmpty()) {
    Map<String, Object> events = emitterRepository.findAllEventCacheStartWithByMemberId(String.valueOf(memberId));
    events.entrySet().stream()
            .filter(entry -> lastEventId.compareTo(entry.getKey()) < 0)
            .forEach(entry -> sendToClient(emitter, entry.getKey(), entry.getValue()));
}
  • lastEventId값이 있는 경우, 저장된 데이터 캐시에서 유실된 데이터들을 다시 전송합니다.

 

Notification - 객체 생성

@Getter
@Entity
@NoArgsConstructor
public class Notification extends Timestamped {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Embedded
    private NotificationContent content;

    @Embedded
    private RelatedUrl url;

    @Column(nullable = false)
    private Boolean isRead;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private NotificationType notificationType;

    @OnDelete(action = OnDeleteAction.CASCADE)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "MEMBER_ID")
    private Member receiver;

    @Builder
    public Notification(Member receiver, NotificationType notificationType, String content, String url) {
        this.receiver = receiver;
        this.notificationType = notificationType;
        this.content = new NotificationContent(content);
        this.url = new RelatedUrl(url);
        this.isRead = false;
    }

    public String getContent() {
        return content.getContent();
    }

    public String getUrl() {
        return url.getUrl();
    }

    public void read(){
        isRead = true;
    }
}
  • Notification 객체를 생성해줍니다.
  • 필요한 값들 (receiver, notificationType, content, url, isRead) 를 넣어줍니다.
    • @Embedded 는 객체를 엔티티로 사용하는게 아니라 값 타입으로 사용하기 위해 붙여줍니다. 좀 더 객체지향적이고 각 상황에 따라 수정이 용이합니다.
    • isRead 는 조회 여부를 알기 위해 Boolean 타입으로 넣었습니다.
    • notificationType은 서비스도메인 별로 enum으로 구분해서 작성했습니다. 저희 프로젝트에서 보내야 하는 알림 종류는 5가지입니다. (콜라보 요청, 콜라보 승인, 게시글 댓글, 게시글 좋아요, 댓글 좋아요)
    • @OnDelete 에 대해서는 여기 블로그를 참조해주세요. receiver를 삭제하면 연관관계도 함께 삭제됩니다.  

 

NotificationContent

@Getter
@Embeddable
@NoArgsConstructor
public class NotificationContent {
    @Column(nullable = false)
    private String content;

    public NotificationContent(String content){
        this.content = content;
    }
}

 

RelatedUrl

@Getter
@Embeddable
@NoArgsConstructor
public class RelatedUrl {
    @Column(nullable = false)
    private String url;

    public RelatedUrl(String url){
        this.url = url;
    }
}

 

 

Entity를 만들었으니 Repository도 만들어줍니다.

 

NotificationRepository

public interface NotificationRepository extends JpaRepository<Notification, Long> {
    List<Notification> findAllByReceiver(Member member);
}
  • JpaRepository를 상속합니다.
  • findAllByReceiver는 알림 전체 목록을 조회할 때 사용한 메서드 입니다.

 

EmitterRepository

public interface EmitterRepository {
    SseEmitter save(String emitterId, SseEmitter sseEmitter);
    void saveEventCache(String emitterId, Object event);
    
    Map<String, SseEmitter> findAllEmitterStartWithByMemberId(String memberId);
    Map<String,Object> findAllEventCacheStartWithByMemberId(String memberId);
    
    void deleteById(String emitterId);
}
  • findAllEmitterStartWithByMemberId 는 해당 member와 관련된 모든 emitter를 찾습니다.
  • findAllEventCacheStartWithByMemberId 는 해당 member와 관련된 모든 event를 찾습니다.

 

EmitterRepositoryImpl

위의 EmitterRepository를 아래와 같이 구현합니다.

public class EmitterRepositoryImpl implements EmitterRepository {
    private final Map<String, SseEmitter> emitters = new ConcurrentHashMap<>();
    private final Map<String, Object> eventCache = new ConcurrentHashMap<>();

    @Override
    public SseEmitter save(String emitterId, SseEmitter sseEmitter) {
        emitters.put(emitterId,sseEmitter);
        return sseEmitter;
    }

    @Override
    public void saveEventCache(String emitterId, Object event) {
        eventCache.put(emitterId,event);
    }

    @Override
    public Map<String, SseEmitter> findAllEmitterStartWithByMemberId(String memberId) {
        return emitters.entrySet().stream()
                .filter(entry -> entry.getKey().startsWith(memberId))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    }

    @Override
    public Map<String, Object> findAllEventCacheStartWithByMemberId(String memberId) {
        return eventCache.entrySet().stream()
                .filter(entry -> entry.getKey().startsWith(memberId))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    }

    @Override
    public void deleteById(String emitterId) {
        emitters.remove(emitterId);

    }

    @Override
    public void deleteAllEmitterStartWithId(String memberId) {
        emitters.forEach(
                (key,emitter) -> {
                    if(key.startsWith(memberId)){
                        emitters.remove(key);
                    }
                }
        );
    }

    @Override
    public void deleteAllEventCacheStartWithId(String memberId) {
        eventCache.forEach(
                (key,emitter) -> {
                    if(key.startsWith(memberId)){
                        eventCache.remove(key);
                    }
                }
        );

    }
}
  • 동시성을 고려하여 ConcurrentHashmap을 사용합니다.
    • HashMap은 멀티스레드 환경에서 동시에 수정을 시도하는 경우 예상하지 못한 결과가 발생할 수 있습니다. 멀티스레드 환경하에서 HashMap을 안전하게 사용하기위해 java에서는 concurrent 패키지를 제공합니다. ConcurrentHashmap을 사용하면 thread-safe가 보장됩니다. ConcurrentHashmap 관련 설명은 여기 블로그를 참조하세요.
    • 그냥 사용한다면 ConcurrentModificationException이 발생할 수 있습니다. 

 

Dto와 MapStruct

추가적으로 저희 프로젝트 컨벤션이기 때문에 MapStruct도 작성해주었습니다. 

@Mapper
public interface SseMapStruct {
    SseMapStruct SSE_MAPPER = Mappers.getMapper(SseMapStruct.class);

    ResponseNotificationDto NotificationtoResponseNotificationDto(Notification notification);

}

ResponseDto도 작성해주었습니다.

@Schema(description = "알림 Dto")
@Getter
@Setter
public class ResponseNotificationDto {
    private Long id;
    private String content;
    private String url;
    private Boolean isRead;
    private String createdAt;

    @Builder
    public ResponseNotificationDto(Notification notification) {
        this.id = notification.getId();
        this.content = notification.getContent();
        this.url = notification.getUrl();
        this.isRead = notification.getIsRead();
        this.createdAt = LocalDateTimeConverter.timeToString(notification.getCreatedAt());
    }

}
  • 프로젝트에서 사용하는 LocalDateTimeConverter를 사용해서 알림 발송시간을 한글로 바꿔줍니다😎

 

 

이제 클라이언트에 알림을 보낼 준비가 끝났습니다.

 

 

NotificationService - 클라이언트에 데이터 전송하기

public void send(Member receiver, NotificationType notificationType, String content, String url) {
    Notification notification = notificationRepository.save(createNotification(receiver, notificationType, content, url));
    String memberId = String.valueOf(receiver.getId());

    Map<String, SseEmitter> sseEmitters = emitterRepository.findAllEmitterStartWithByMemberId(memberId);
    sseEmitters.forEach(
            (key, emitter) -> {
                emitterRepository.saveEventCache(key, notification);
                sendToClient(emitter, key, SSE_MAPPER.NotificationtoResponseNotificationDto(notification));
            }
    );
}

private void sendToClient(SseEmitter emitter, String emitterId, Object data) {
    try {
        emitter.send(SseEmitter.event()
                .id(emitterId)
                .data(data));
    } catch (IOException exception) {
        emitterRepository.deleteById(emitterId);
        throw new InvalidRequestException(SSE, SERVICE, UNHANDLED_SERVER_ERROR);
    }
}

 

Notification을 전송하기 위한 메서드 입니다.

public void send(Member receiver, NotificationType notificationType, String content, String url) {
    Notification notification = notificationRepository.save(createNotification(receiver, notificationType, content, url));
    String memberId = String.valueOf(receiver.getId());

    Map<String, SseEmitter> sseEmitters = emitterRepository.findAllEmitterStartWithByMemberId(memberId);
    sseEmitters.forEach(
            (key, emitter) -> {
                emitterRepository.saveEventCache(key, notification);
                sendToClient(emitter, key, SSE_MAPPER.NotificationtoResponseNotificationDto(notification));
            }
    );
}
  • Notification 객체를 만들고 해당 Member의 emitter를 다 불러옵니다.(여러 브라우저에서 접속할 수 있기 때문에 emitter가 여러개 일 수 있습니다.)
  • 해당 데이터를 EventCache에 저장합니다.
  • sendToClient 메서드를 통해 클라이언트에 전송합니다.
    • MapStruct를 사용해서 Dto로 변환한 값을 보내줬습니다.

 

private void sendToClient(SseEmitter emitter, String emitterId, Object data) {
    try {
        emitter.send(SseEmitter.event()
                .id(emitterId)
                .data(data));
    } catch (IOException exception) {
        emitterRepository.deleteById(emitterId);
        throw new InvalidRequestException(SSE, SERVICE, UNHANDLED_SERVER_ERROR);
    }
}
  • emitter, emitterId와 함께 알림 내용을 클라이언트에 보냅니다.
  • 전송이 안된 경우 IOException을 터트려줍니다. IOException은 스트림, 파일 및 디렉터리를 사용해 정보에 엑세스하는 동안 throw된 예외에 대한 기본 클래스입니다. 

 

 

알림 만들기 🛎

이제 위에서 만든 알림 메서드를 호출해서 콜라보요청이 승인 되면 콜라보 요청 작성자에게 알림을 보내는 기능을 추가해보겠습니다.

@Transactional
public void approveCollaboRequest(Long collaborequestid, Member member) {
    /.../

    //요청한 사람한테 승인 완료 알림 - 게시글 상세 조회로 이동
    Long postId = post.getId();
    Member collaboMember = memberRepository.findByNickname(collaboRequest.getNickname())
            .orElseThrow(() -> new NotFoundException(COLLABO_REQUEST, SERVICE, MEMBER_NOT_FOUND));
    String url = "/api/post/"+postId;
    String content = post.getTitle()+"에 대한 콜라보 요청이 승인되었습니다.";
    notificationService.send(collaboMember, NotificationType.COLLABO_APPROVED, content, url);
}
  • 콜라보 요청을 한 member가 receiver이고, 해당 Post의 상세조회 페이지의 Url을 넣었습니다. NotificationType으로는 미리 enum으로 저장해 놓은 COLLABO_APPROVED(콜라보 승인)을 넣어줬습니다.

 

알림 결과는 아래와 같이 Postman 으로 확인할 수 있습니다.

클라이언트 사이드는 구현은 아직 안해서 알림 전체 조회하는 api를 만들어서 조회했습니다. 

 

 

 

 

참조 문서

여러 블로그를 참조해서 작성했습니다. 주요 참고한 블로그는 아래와 같습니다.

 

 

[Spring + SSE] Server-Sent Events를 이용한 실시간 알림

코드리뷰 매칭 플랫폼 개발 중 알림 기능이 필요했다. 리뷰어 입장에서는 새로운 리뷰 요청이 생겼을 때 모든 리뷰가 끝나고 리뷰이의 피드백이 도착했을 때 리뷰이 입장에서는 리뷰 요청이 거

velog.io

 

알림 기능을 구현해보자 - SSE(Server-Sent-Events)!

시작하기에 앞서 이번에 개발을 진행하면서 알림에 대한 요구사항을 만족시켜야하는 상황이 발생했다. 여기서 말하는 알림이 무엇인지 자세하게 살펴보자. A라는 사람이 스터디를 생성했고 B라

gilssang97.tistory.com

 

Node긴 하지만 개념 정리가 잘 되있어서 아래 블로그도 공유합니다.

 

[NODE] 📚 Server Sent Events 💯 정리 (+사용법)

SSE - Server Sent Events 란? SSE는 서버의 데이터를 실시간으로, 지속적으로 Streaming 하는 기술 이다. SSE는 웹 표준으로써 IE를 제외한 모든 브라우저에서 지원되며, IE역시 polyfill을 통해 지원이 가능하

inpa.tistory.com

 

 

 

오늘부터 MVP 2차를 개발하기 시작했습니다. 알람 기능 구현이 제가 맡은 부분입니다. 프론트엔드가 2명밖에 없어서 알람까지 할 수 있을지는 모르겠지만 일단은 만들고 생각하기로 했습니다. 프론트엔드분들도 우는소리 하시면서 척척 해내시더라고요. 저희 프론트엔드분들도 그렇고 백엔드분들도 능력자들만 있어서 참 팀원운이 좋다고 생각합니다.

 

구현해야하는 기능

1. 승인요청알람

2. 승인완료알람

3. 게시글 좋아요 알람

4. 댓글 좋아요 알람

5. 댓글 등록 알람

 

처음에는 웹소켓으로 해야하나라고 생각했는데 서버에서 클라이언트에 알람을 보내는 단방향 형식이기 때문에 SEE라는 것을 사용하기로 했습니다. 웹소켓(WebSocket)은 클라이언트와 서버 간의 효율적인 양방향 통신을 실현하기 위한 구조입니다. 채팅과 같이 클러이언트와 서버가 양방향 통신이 필요하면 웹소켓을 쓰겠지만 알람은 서버만 클라이언트에게 정보를 보내는 구조입니다. 

 

추후에 DM을 구현하게 된다면 웹소켓을 사용할지 다시 검토해봐야 겠습니다.

 

 

SSE(Server-Sent-Events)

전통적으로 웹에서는 새 데이터를 수신하기 위해 서버에 요청을 보내야 합니다. 클라이언트가 서버에 데이터를 요청합니다. SSE를 사용하면 서버가 클라이언트에 메시지를 푸시하여 언제든지 새 데이터를 클라이언트로 보낼 수 있습니다. 이러한 수신 메시지는 클라이언트의 이벤트 + 데이터로 처리될 수 있습니다. 

 

Server-sent events - Web APIs | MDN

Traditionally, a web page has to send a request to the server to receive new data; that is, the page requests data from the server. With server-sent events, it's possible for a server to send new data to a web page at any time, by pushing messages to the w

developer.mozilla.org

 

SSE는 웹소켓과 비슷하지만 단방향 통신입니다. 클라이언트는 서버로 데이터를 보낼 수 없습니다. 웹소켓과 달리 별도의 프로토콜을 사용하지 않아서 훨씬 가볍다고 합니다.  HTTP 스트리밍을 통해 서버에서 클라이언트로 단방향의 Push Notification을 전송할 수 있는 HTML5 표준 기술입니다.  

SSE는 서버와 한번 연결을 맺고나면 일정 시간동안 서버에서 변경이 발생할 때마다 데이터를 전송합니다.

 

위의 mozilla.org 사이트에서 보여준 예시는 자바스크립트인데, 자바스크립트에서는 EventSource를 이용해서 구현할 수 있습니다. Spring Framework 4.2 부터 SseEmitter 클래스를 제공해 SSE 통신을 구현할 수 있습니다.

 

1. 클라이언트가 서버의 이벤트를 subscribe하기 위한 요청을 보낸다.

2. 서버가 이벤트를 전송한다.

 

주의할점(다른 블로그에서 퍼옴)

1. 첫 SSE 응답 보낼 때, 더미 데이터를 넣어야 503 에러가 발생하지 않음

2. JPA 사용시 open-in-view 설정을 false로 하기

3. Ngnix 사용시 1.1 version으로 설정 

 

 

참고 블로그

 

Spring Boot, SSE(Server-Sent Events)로 단방향 스트리밍 통신 구현하기

개요 Server-Sent Events(이하 SSE)는 HTTP 스트리밍을 통해 서버에서 클라이언트로 단방향의 Push Notification을 전송할 수 있는 HTML5 표준 기술이다. 이번 글에서는 Spring Boot에서 SSE를 이용한 단방향 스트

jsonobject.tistory.com

 

 

 

알림 기능을 구현해보자 - SSE(Server-Sent-Events)!

시작하기에 앞서 이번에 개발을 진행하면서 알림에 대한 요구사항을 만족시켜야하는 상황이 발생했다. 여기서 말하는 알림이 무엇인지 자세하게 살펴보자. A라는 사람이 스터디를 생성했고 B라

gilssang97.tistory.com

 

 

[Spring + SSE] Server-Sent Events를 이용한 실시간 알림

코드리뷰 매칭 플랫폼 개발 중 알림 기능이 필요했다. 리뷰어 입장에서는 새로운 리뷰 요청이 생겼을 때 모든 리뷰가 끝나고 리뷰이의 피드백이 도착했을 때 리뷰이 입장에서는 리뷰 요청이 거

velog.io

 


 

아침에는 오랜만에 헬스를 다녀왔습니다. 부트캠프 시작하고 몸무게는 변동은 없지만 체성분이 변했습니다. 스쿼트를 40kg 10x3 set 밖에 못해서 놀랐습니다. 근육 놀랄까봐 무게를 더 치진 않았습니다. 2월 17일에 부트캠프가 끝나는데 다시 스쿼트랑 데드 70 들던 때로 돌아가겠습니다.  개발자지망생이 되니 다시 거북목이 심해져서 내일은 등이랑 어깨를 뿌실겁니다. 그리고 알람은.... 내일까지 해보자!!!

 

 

저희 팀에서 구상하고 있는 프로젝트의 핵심 기능은 콜라보 리퀘스트 요청입니다.

 

1. 기존의 게시물에 콜라보를 하고 싶으면 콜라보 리퀘스트를 보냅니다.

2. 콜라보 리퀘스트를 받은 게시물의 작성자가 승인을 하면

3. 게시글 조회시 승인된 콜라보 리퀘스트의 목록이 함께 조회가 됩니다.

 

제가 맡은 도메인이 바로 이 콜라보리퀘스트입니다. 깃헙의 pull request에서 영감을 받은 기능입니다.

현재 CRUD의 CR만 구현된 상태입니다. 승인을 위해 보여지는 상세 페이지와 승인이 완료된 콜라보리스트를 보여주는 기능을 각각 구현했습니다.

 

수정 기능과 삭제 기능은 오늘 만들어 놓으려고 합니다.

승인되기 전에만 수정 또는 삭제 할 수 있게 합니다.

 

 

위의 기능들은 CRUD이기 때문에 기존에 했던 프로젝트와 크게 다르지는 않습니다.

그래서 제가 욕심을 내고 있는 기능은 알람기능입니다.

 

원하는 기능은 아래와 같습니다.

 

1. 콜라보 리퀘스트를 작성하면, 해당 게시글의 작성자에게 승인 요청 알람 보내기

2. 승인이 완료되면, 해당 콜라보 리퀘스트 작성자에게 승인 완료 알람 보내기

 

CRUD가 끝나면 깊이 파볼 생각입니다.

 

List 수정 하는 법

삭제하는 법은 querydsl을 사용해서 처리했는데 수정은 모르겠어서 일단은 삭제 후 새로 저장하는 방법으로 구현했습니다.

 

수정 시, musicList의 music의 갯수가 늘어나거나 줄어드는 경우가 있어서 뭔가 복잡한 느낌이 듭니다. 

 

결국 알아낸 방법은 똑같이 query를 수정해주면 된다는 것이다.

삭제할 때는 다음과 같은 코드를 사용해서 musicList를 삭제해주었다. Music은 CollaboRequest를 외래키로 가지고 있다.

 

@Transactional
@Modifying
@Query("DELETE from Music c where c.collaboRequest = :collaboRequest")
void deleteAllByCollaboRequest(@Param("collaboRequest") CollaboRequest collaboRequestId);

 

QueryDSL

  1. 문자가 아닌 코드로 쿼리를 작성함으로써, 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다.
  2. 자동 완성 등 IDE의 도움을 받을 수 있다.
  3. 동적인 쿼리 작성이 편리하다.
  4. 쿼리 작성 시 제약 조건 등을 메서드 추출을 통해 재사용할 수 있다.

 

QueryDSL이란 것으로 쿼리를 더 쉽게 커스텀해서 사용할 수 있다.

이제 알았으니 Update를 위한 query를 만들어보자.

https://stackoverflow.com/questions/31937540/update-list-of-entities-in-one-query-jpa

 

Update List of entities in one query jpa

I need to update a list of objects in one query. I have class Parent which has a list of Child in ManyToMany relation. I need to update a list of Parent by setting children list to empty(remove all

stackoverflow.com

개발자의 친구 stackOverFlow를 참조해서 키워드를 찾았다.

 

아래와 같이 하면 된다고 한다.

UPDATE [테이블] SET [] = '변경할값' WHERE [조건]

 

좀 더 고민해봐야겠다...

어제 거의 2시간만에 콜라보리퀘스트 승인 기능을 만들고, 승인한 게시글만 불러오는 기능을 만들었습니다. 

처음 부트캠프를 시작했을 때에 비하면 장족의 발전입니다. 이제는 스스로 생각해서 코딩하는 경우가 더 많아졌습니다. 물론 아직까지는 다른 사람의 코드를 보고 배우는 점이 더 많긴 합니다.

 

다른분들이 아침8시부터 같이 공부한다고 하니까 저도 내일부터는 8시에 출근 하려고요.

 

 

이제 코딩배운지 4개월차인 개발자 지망생입니다.

 

오늘 실전프로젝트 첫 멘토링이 있었습니다.

일주일간의 회고와 프로젝트에 대한 질문답변과 조언을 들었습니다.

SOLID 원칙

SOLID 원칙

클린 코드에 대한 이야기가 나왔는데 멘토님이 SOLID 원칙을 말씀해주셔서 찾아보았습니다.

로버트 C. 마틴이 2000년에 쓴 논문 Design Principles and Design Patterns에서 software rot에 대해 설명하면서 처음 소개된 개념입니다. 

S Single responsibility principle (단일책임원칙) - 하나의 클래스는 하나의 책임만 가진다.
O Open-closed principle (개방-폐쇄원칙) - 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다.
L Liskov substitution principle (리스코프 치환 원칙) - 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
I Interface segregation principle (인터페이스 분리 원칙) - 특정 클라이언트를 위한 인터페이스 여러 개가 인터페이스 하나보다 낫다.
D Dependency Inversion principle (의존관계 역전 원칙) - 프로그래머는 "추상화에 의존하며, 구체화에 의존하면 안된다."

 

2004년에는 Michale Feathers에 의해 SOLID 라는 약어로 소개되었습니다.

 

SOLID 원칙들은 어떠한 객체지향 디자인에도 적용할 수 있습니다.

애자일 개발 또는 Adaptive software development(ASD)의 방식의 핵심 철학이 될 수 있습니다.

 

 

 

출처:

 

SOLID - Wikipedia

Object-oriented software engineering design principles This article is about the SOLID principles of object-oriented programming. For the fundamental state of matter, see Solid. For other uses, see Solid (disambiguation). In software engineering, SOLID is

en.wikipedia.org

 

 

실전주차 1주차에 대한 회고

솔직히 이번주차는 개인적으로 중요한 큰 이슈가 있어서 부트캠프에 100% 집중하지는 못했습니다.

하지만 맡은 역할은 다 구현을 했고 지금은 관련해서 refactoring을 하고 있습니다.

mapstruct도 처음 써봤고 직접 swagger를 적용하는 것도 처음입니다.

다른 분들의 저와는 달리 깔끔해보이는 코드를 보면서 반성도 하고 제 꺼에 적용도 해보면서 발전해나가고 있습니다.

 

생각보다 API 명세서를 수정해야하는 부분이 많이 보여서 처음에 설계한 대로 구현하긴 했지만 refactoring 해야할 부분들이 많이 보입니다.

저희 프로젝트는 백엔드 보다는 프론트엔드 부분의 역량이 중요해서 백엔드에 대해서 어떤 도전적인 모습을 보여줄 수 있을까 고민도 했는데 굳이 고민안해도 될 것 같습니다. 생각보다 구현이 어려운 부분이 많습니다. 

 

그래도 부트캠프 처음 들어왔을 때와 지금을 비교하면 천지차이라고 할 수 있습니다.

간단한 CRUD는 그냥 합니다.

 

이번주차에서 어려웠던 것은 한 request안에 여러 Entity들에 들어가는 값들을 받아와서 따로 그 Entity에 맞는 repository에 저장을 해주는 것이었습니다. 반대로는 (다른 Repository에 있는 정보를 하나의 Response Dto에 넣어서 보내주기)는 했는데 반대를 어떻게 해야할 지에 대한 고민이 있었습니다.

 

결론은 한 requestDto에 여러 entity의 값들을 받아와서 controller에서 두개의 dto로 나눠 service에 전달을 하는 거였습니다. 

 

코드 공유를 살짝 하자면 아래와 같습니다. MapStruct는 사랑입니다.

 

  @PostMapping("/api/post/{postid}/collabo")
    public ResponseEntity<?> collaboRequest(@PathVariable Long postid, @RequestBody RequestCollaboRequestDto requestCollaboRequestDto, @ApiIgnore @AuthenticationPrincipal CustomUserDetails customUserDetails){
        collaboRequestService.collaboRequest(postid, requestCollaboRequestDto.tocollaboRequestDetailsDto(), requestCollaboRequestDto.tosaveMusicDto(), customUserDetails.getMember());

        return SuccessResponse.toResponseEntity(COLLABO_REQUEST, null);

 

tocollaboRequestDetailsDto() 와 toSaveMusicDto()로 RequestCollaboRequestDto로 받아온 값들을 나눠서 service에 전달해주었습니다.

 

 

지금은 처음에 Musicfile를 하나만 작성할 수 있었는데 API 명세를 보니 조회할때는 musicList로 받아와서 여러개를 requestDto에 담을 수 있도록 수정하고 있습니다. 오늘 이거는 해놓고 퇴근하려고 합니다. 

 

 

 

 

실전프로젝트를 진행하면서 팀원들과 github을 활용하고 있다.

이슈를 만들고 로컬에서 작업하고 푸시하고 PR한다.

Pull Request를 함으로써

서로의 코드 리뷰까지 함께 진행하게 되는데 팀원들의 깔끔한 코드를 보면서 반성하게 된다.

뭔가 클린 코딩에 대해서는 나중에 생각하고 기능부터 구현하자가 되버려서 그런거 같다.

내가 구현 속도가 가장 느리기도 하다.

굳이 변명을 하자면 전공자와 국비 수료자 사이에 있다.

나는 이 부트캠프가 코딩 배우는건 아예 처음이고.

 

여튼 변명은 여기까지고,

다른 분들의 코드를 보면 자극이 된다.

어떻게 이렇게 이쁘게 잘짰지? 하는.

 

머리를 쓰고 그다음에 타이핑을 해야겠다.

타이핑하면서 머리를 쓰니

주먹구구식이 되는 듯하다.

 

전에 다른 팀원분이 주신 객체지향 생활체조를 다시한번 정독해야겠다.

 

1. 한 메서드에 오직 한 단계의 들여쓰기만 한다.
2. else 키워드를 쓰지 않는다
3. 모든 원시값과 문자열을 wrap 한다.
4. 한 줄에 점을 하나만 찍는다.
5. 줄여쓰지 않는다.
6. 모든 entity를 작게 유지한다.
7. 2개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
8. 일급 컬렉션을 쓴다
9. getter/setter/property를 쓰지 않는다

+ Recent posts