로그인을 하지 않은 유저가 게시글을 상세 조회 했을 때, 좋아요 여부와 팔로우 여부는 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를 수정하고 있습니다.
웹개발자가 되겠다고한지 벌써 5개월차입니다. 친한 지인이 퇴사하고 부트캠프 할거라고 해서 이미 고연봉자인 지인이 한다니 호기심이 생겼던 게 지난 9월이었습니다. 가족이 새로 하는 사업 때문에 백수였던 제가 도와드리고 있었는데 '웹사이트를 만들어봐라' 하셔서 돈주고할려다가 너무 비싸서 제가 만들기 시작하고 있었던 것도 한 몫 했습니다. 웹개발 툴(아임웹)을 쓰고 있었는데 간단한 html 코드 정도 써보고 있던 수준이었습니다. 부트캠프의 존재도 모르고 있었는데 어떻게 벌써 이렇게 한달밖에 안남은 시점이 되었네요.
TIL
이제는 CRUD 정도는 척척하게 되었습니다. 처음에는 "왜 다 안알려주고 이렇게 불친절한 부트캠프를 봤나!" 하고 살짝은 불만이었던 부분들이 어느정도 해소가 되었습니다. 하다보면 자연히 필요에 의해 찾아볼 수 밖에 없습니다. 그래도 저는 여전히 제가 하고 있는 항해99를 추천하냐고 묻는다면 공부를 아주 열심히 해본 적 있는 분들만, 어느정도 이과적 공부 머리가 있는 분들에게 추천합니다. 뭐... 12시간동안 앉아서 공부할 의지가 있다는 거 자체가 남들과는 다른 의지와 공부 습관을 가진 사람들이라고 할 수는 있겠다는 생각이 듭니다. 부트캠프 신청자체가 진입장벽이라고 생각하면 괜찮을 수도 있겠네요. 저는 원래 공부만 하던 사람이라 괜찮았습니다. 스스로 찾아서 하는 개발자가 되는게 중요하다고 했는데 저는 원래 하던 일도 그래서 괜찮았습니다.
친절하고 열심히하는 동료들을 만나는 것도 큰 행운으로 작용해서 제가 부트캠프에 열정적으로 임하게 되는 요인중에 하나입니다. 저는 아직까지 항해99의 동료들에게 불만이 있었던 적이 없습니다. 다들 너무 감사하고 제가 작년에 있던 불운들은 이분들을 만나기 위해 행운을 아껴놓았던 거라고 생각할 정도입니다. 최종 프로젝트는 6주동안 함께 하는 사람들인데 다들 열정적이고 저보다 코딩 배운지 오래 되신 분들이 대부분이라 배울점이 너무 많은데 친절하시기까지 하고, 새로 들어온 디자이너님도 열정 있으시고 실력도 뛰어나서 또 내게 이런 행운이 왔구나하는 마음에 감사했습니다. 저 또한 이분들에게 좋은 동료가 되고 싶습니다.
남은 한달동안은 최종 프로젝트를 잘 마무리하고 이력서도 쓰고 코딩 테스트 준비도 하고 면접준비도 하면 금방 지나갈 것 같습니다. 지금까지 해왔던 것처럼 앞으로도 꾸준히 무소의 뿔처럼 나아가겠습니다.
영어를 할 줄 아는 개발자가 장점이 많을 거라는 생각이 드네요. 에러 메세지가 한글로 뜨면 참 좋을텐데 말이죠.
아 물론 저는 잘합니다😎
Stacktrace
stack trace는 프로그램 서브루틴에 대한 정보를 제공하는 보고서입니다. 일반적으로 stack trace을 통해 소프트웨어 엔지니어가 문제가 있는 위치 또는 실행 중에 다양한 서브루틴이 함께 작동하는 방식을 파악하는 데 도움이 되는 특정 종류의 디버깅에 사용됩니다.
그리고 저의 일주일 회고를 이야기하는데 이번에 구글링을 하면서 블로그 자료들 중에 정확하지 않은 것도 많고 구린것도 많아서 정보를 보는 눈을 키우고 싶다고 말씀드렸더니 공식문서를 읽으라고 하셨습니다. 부트캠프에서 학생들에게 강제하다보니 기술블로그도 우후죽순 생겨나서 그렇다고 하셨습니다. 앗 근데 저도 괜히 반성하게 되서 기술 관련해서 리뷰할 때는 좀 더 정확하게 써야겠다고 다짐했습니다.
오늘은 알림기능을 위해서 먼저 구현해야하는 좋아요를 복합키를 사용해서 구현해보려고 합니다. 게시글 좋아요, 댓글 좋아요가 필요한데 복합키를 써보는 경험을 다 같이 해보기 위해서 저는 게시글 좋아요 다른 팀원분이 댓글 좋아요를 맡았습니다. 복합키 자체가 자료가 잘 안나오네요. 조장님이 복합키 사용해보라고 했는데 한번 도전해봅니다. 일단 뭔지 알아야겠지요.
Composite Key 복합키
복합키는 Composite Key로 2개 이상의 column을 프라이머리 키로 가지고 있습니다. 복합키는 @IdClass와 @EmbeddedId 어노테이션을 사용해서 정의할 수 있습니다. 복합키는 다음과 같은 규칙을 가지고 있습니다.
SseEmitter 클래스는 SpringFramework에서 버전 4.2 부터 제공합니다.
emitter는 발신기라는 뜻을 가지고 있습니다.
String emitterId = memberId_System.currentImeMillis(); 로 한 이유는 Last-Event-ID와 관련이 있습니다.
Last-Event-ID는 클라이언트가 마지막으로 수신한 데이터의 Id값을 의미합니다. 그러나 Id 값만을 사용한다면 언제 데이터가 보내졌는지, 유실 되었는지 알 수가 없기 때문에 시간을 emitterId에 붙여두면 데이터가 유실된 시점을 알 수 있으므로 저장된 Key값 비교를 통해 유실된 데이터만 재전송 할 수 있습니다.
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이 발생할 수 있습니다.
오늘부터 MVP 2차를 개발하기 시작했습니다. 알람 기능 구현이 제가 맡은 부분입니다. 프론트엔드가 2명밖에 없어서 알람까지 할 수 있을지는 모르겠지만 일단은 만들고 생각하기로 했습니다. 프론트엔드분들도 우는소리 하시면서 척척 해내시더라고요. 저희 프론트엔드분들도 그렇고 백엔드분들도 능력자들만 있어서 참 팀원운이 좋다고 생각합니다.
구현해야하는 기능
1. 승인요청알람
2. 승인완료알람
3. 게시글 좋아요 알람
4. 댓글 좋아요 알람
5. 댓글 등록 알람
처음에는 웹소켓으로 해야하나라고 생각했는데 서버에서 클라이언트에 알람을 보내는 단방향 형식이기 때문에 SEE라는 것을 사용하기로 했습니다. 웹소켓(WebSocket)은 클라이언트와 서버 간의 효율적인 양방향 통신을 실현하기 위한 구조입니다. 채팅과 같이 클러이언트와 서버가 양방향 통신이 필요하면 웹소켓을 쓰겠지만 알람은 서버만 클라이언트에게 정보를 보내는 구조입니다.
추후에 DM을 구현하게 된다면 웹소켓을 사용할지 다시 검토해봐야 겠습니다.
SSE(Server-Sent-Events)
전통적으로 웹에서는 새 데이터를 수신하기 위해 서버에 요청을 보내야 합니다. 클라이언트가 서버에 데이터를 요청합니다. SSE를 사용하면 서버가 클라이언트에 메시지를 푸시하여 언제든지 새 데이터를 클라이언트로 보낼 수 있습니다. 이러한 수신 메시지는 클라이언트의 이벤트 + 데이터로 처리될 수 있습니다.
SSE는 웹소켓과 비슷하지만 단방향 통신입니다. 클라이언트는 서버로 데이터를 보낼 수 없습니다. 웹소켓과 달리 별도의 프로토콜을 사용하지 않아서 훨씬 가볍다고 합니다. HTTP 스트리밍을 통해 서버에서 클라이언트로 단방향의 Push Notification을 전송할 수 있는 HTML5 표준 기술입니다.
SSE는 서버와 한번 연결을 맺고나면 일정 시간동안 서버에서 변경이 발생할 때마다 데이터를 전송합니다.
위의 mozilla.org 사이트에서 보여준 예시는 자바스크립트인데, 자바스크립트에서는 EventSource를 이용해서 구현할 수 있습니다. Spring Framework 4.2 부터 SseEmitter 클래스를 제공해 SSE 통신을 구현할 수 있습니다.
아침에는 오랜만에 헬스를 다녀왔습니다. 부트캠프 시작하고 몸무게는 변동은 없지만 체성분이 변했습니다. 스쿼트를 40kg 10x3 set 밖에 못해서 놀랐습니다. 근육 놀랄까봐 무게를 더 치진 않았습니다. 2월 17일에 부트캠프가 끝나는데 다시 스쿼트랑 데드 70 들던 때로 돌아가겠습니다. 개발자지망생이 되니 다시 거북목이 심해져서 내일은 등이랑 어깨를 뿌실겁니다. 그리고 알람은.... 내일까지 해보자!!!