멍냥의 전당 좋아요

어제는 오랜만에 일찍 체크아웃을 했습니다. 주특기인 스프링을 배우는 3주동안 거의 계속 새벽 2시, 3시 심하면 5시에 잠들었었거든요. 자바를 접한지 한달, 스프링을 접한지 3주동안 쉼없이 계속 달렸습니다. 일찍이라고 해봤자 사실 11시 반쯤 맥북프로를 닫았던 것 같습니다. 내일은 제 생일이라 오늘은 9시 칼퇴 예정입니다. 

 

저는 이번에 진행하는 첫 미니프로젝트에서 좋아요 기능을 담당하고 있습니다. 고수분들(왜 취직 안하고 부트캠프에 온건지 심히 의문이 드는)이 많아서 조금 걱정이 되긴 합니다만 그래도 좋아요 기능 오늘 완성 했습니다.

 

그랬더니 조장님이 다른 미션을 던져 주셨네요. 

 

게시판과 댓글을 연관관계 설정 하고 나서 게시글을 지우면 댓글도 다 같이 지워지게 CASCADE.REMOVE 를 사용했었는데 이를 사용하면 위험(?) 하다고 하니 다른 방법으로 DELETE 요청을 구현하라는 미션입니다. 일단 왜 CASCADE.REMOVE가 위험한지를 알아야겠네요. 다음주의 미션이고 일단은 어제 밤에 진행됐던 프론트앤드, 백앤드 간의 서버 통신 및 배포에 관한 세션을 복습 할 예정입니다.

 

아래는 오늘 구현 완료한 좋아요 기능 입니다. @Service 부분만 공유할게요.

 

LikeService

package com.hanghae99.catsanddogs.service;

import com.hanghae99.catsanddogs.dto.LikePostResponseDto;
import com.hanghae99.catsanddogs.dto.ResponseMessage;
import com.hanghae99.catsanddogs.entity.*;
import com.hanghae99.catsanddogs.exception.CustomException;
import com.hanghae99.catsanddogs.exception.ErrorCode;
import com.hanghae99.catsanddogs.repository.CommentRepository;
import com.hanghae99.catsanddogs.repository.LikeCommentRepository;
import com.hanghae99.catsanddogs.repository.LikePostRepository;
import com.hanghae99.catsanddogs.repository.PostRepository;
import com.hanghae99.catsanddogs.security.UserDetails.UserDetailsImpl;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Getter
@RequiredArgsConstructor
@Service
public class LikeService {

    private final PostRepository postRepository;
    private final CommentRepository commentRepository;
    private final LikePostRepository likePostRepository;
    private final LikeCommentRepository likeCommentRepository;

    @Transactional
    public boolean likePost(Long postId, User user) {
        Post post = postRepository.findById(postId).orElseThrow(
                () -> new CustomException(ErrorCode.CONTENT_NOT_FOUND)
        );
        Long userId = user.getId();

        //좋아요 했는지 확인
        Optional <LikePost> likePost = likePostRepository.findByPostIdAndUserId(postId, userId);

        if (likePost.isPresent()) {
            LikePost like = likePost.get();
            likePostRepository.delete(like);

            return false;

        } else{
            LikePost like = new LikePost(postId, userId);
            likePostRepository.save(like);
            return true;
        }
    }

    public boolean likeComment(Long commentId, User user) {
        Comment comment = commentRepository.findById(commentId).orElseThrow(
                () -> new CustomException(ErrorCode.COMMENT_NOT_FOUND)
        );
        Long userId = user.getId();

        Optional<LikeComment> likeComment = likeCommentRepository.findByCommentIdAndUserId(commentId, userId);

        if(likeComment.isPresent()){
            LikeComment like = likeComment.get();
            likeCommentRepository.delete(like);

            return false;

        } else{
            LikeComment like = new LikeComment(commentId, userId);
            likeCommentRepository.save(like);
            return true;
        }
    }
}

어제 댓글 구현을 stream을 통해 구현을 했는데 @JsonIgnore를 사용하면 순환참조를 방지할 수 있다는 이야기를 들었습니다. 지금은 아직 배우는 단계이기 때문에 순수 자바를 이용해서 for문을 사용해 댓글 목록을 조회하는 것을 추천받기는 했습니다. 

 

오늘은 첫 포트폴리오 프로젝트를 시작했습니다. 제가 기획한 프로젝트가 선택되었는데 프로젝트 명은 '멍냥의 전당'으로 반려동물의 사진과 함께 게시글을 올리고 공유하는 웹사이트입니다. 요즘 '개발바닥'이라는 유튜브를 정주행하고 있는데 그 채널에 나오는 호돌맨님이 다니는 회사가 반려생활이라고 해서 찾아봤었는데 아마 거기서 아이디어가 떠오르지 않았나 싶습니다. 조원들에게 피칭할 때는 저희 온라인 부트캠프 한다고 캠을 켜놓고 있다보면 조원들의 강아지나 고양이가 왔다갔다하는 것을 볼 수 있는데 모두에게 자신의 반려동물을 자랑하고 보여줄 수 있는 웹사이트를 만들면 어떻겠냐고 했습니다. 혹시나 나중에 반려생활같은 반려동물 관련 서비스에 지원한다면 첫 포트폴리오가 도움이 될 수도 있다고 생각했습니다. 물론 기술 스택이 중요하겠지만요.

 

저는 댓글 좋아요, 게시글 좋아요 기능을 담당하기로 했습니다. 이번 주 내내 댓글을 연구했어서 좋아요 기능까지 못했거든요. 프론트 앤드 분들과의 첫 협업인데 조원들 중에 실력자가 많아서 든든합니다. 다들 왜 부트캠프를 하는지 이해가 안될 정도 입니다. 저는 이제 한달이 조금 지난 왕초보 개발자 지망생인데 말이죠. 처음에 비전공자를 위한 부트캠프라고 홍보해서 들어왔는데 전공자분들이 더 많은 거 같아서 당황스럽기는 합니다. 마케팅 타겟은 물론 비전공자 쪽이 훨씨 많아서 그럴수 있다고 생각합니다. 사업이 더 커질려면 인구가 많은 쪽을 공략해서 파이를 키우는게 맞지만 너무 이렇게 바를 올려버린다는 느낌이 들면 아예 처음 시작하는 사람한테는 이 부트캠프를 추천할 수는 없을 것 같습니다. 어느정도 컴퓨터 프로그래밍에 대한 지식이 있는, 최소로 본인이 선택할 주특기의 언어 정도는 이미 좀 익숙한 상태에서 지원한다면 괜찮을 수도 있다고 생각합니다. 하지만 저는 시작하기 직전에 주특기 설명 세션을 통해 결정을 했기 때문에 미리 공부할 시간 따위는 없었다고 합니다. 그래도 새벽 2~3시까지 매일 몰입하면서 경험치가 오르고 있다는 게 느껴지기는 하지만 아직 레벨 0, 튜토리얼에 머물러 있기는 합니다. 앞으로 미니 프로젝트를 진행하고, 클론 프로젝트, 실전프로젝트까지 하고 2월에 수료를 하게 되면 드디어 개발자 레벨 1 정도라고 할 수 있지 않을까 기대해봅니다. 

터미널로 스프링 실행하기

 

cd 파일명

수업 예제에서는 hello-spring을 사용

 

cd hello-spring

 

./gradlew build
cd build
cd libs
ls -arlth
java -jar hello-spring-0.0.1-SNAPSHOT.jar

 

스프링 부트가 실행되고 잘 작동하는 것을 확인할 수 있습니다. 

 

실행 종료는 아래 명령를 이용해서 종료합니다.

 

ls -arlth

 

안되는 경우,

 

./gradlew clean  build 

 

로 다시 지우고 새로 시작할 수 있습니다. 

 

 

 

 

 

JPA를 활용하여 연관관계를 맺어줘서 게시글 하나만 삭제해도 관련된 댓글이 다 삭제가 되고, 게시글 하나만 불러도 연관된 댓글이 몽땅 다 삭제되는 것을 구현하는 것에 성공했습니다. JPA는 테이블을 객체처럼 사용한다에서 흰트를 얻어서 객체처럼 넣어주고 값을 불러주고 하는 방법으로 구현했습니다. 복잡한 코드를 작성하지 않더라도 전체 게시글 조회할 때 각 게시글에 연관된 댓글들이 함께 조회가 됩니다. 어제는 중첩 for문을 돌려서 전체 게시글(+댓글) 조회를 했는데 JPA로 for문 필요 없이 간단하게 불러오게 했습니다.

am....I ..... genius? no... jjajipgi dal-in

 

JPA 연관관계 사용 게시판, 댓글 구현하기

 

Post

package com.example.post.entity;

import com.example.post.dto.PostRequestDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

@Getter
@Entity
@NoArgsConstructor
public class Post extends Timestamped{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String content;

    @Column(nullable = false)
    private String username;
//
    @ManyToOne(fetch = FetchType.LAZY)
    private User user;


// mappedBy로 연관관계 주인 설정, ManytoOne 다대일 관계설, "comment"가 외래키를 관리
    @OneToMany(mappedBy = "post", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
    private List<Comment> commentLists = new ArrayList<>();

    public Post(PostRequestDto requestDto, User user){
        this.content = requestDto.getContent();
        this.title = requestDto.getTitle();
        this.username = user.getUsername();
        this.user = user;

    }

    public void update(PostRequestDto requestDto) {
        this.content = requestDto.getContent();
        this.title = requestDto.getTitle();
    }
}
  • @ManyToOne으로 User 와의 관계를 설정해주었습니다. post를 작성한 user의 정보를 받아 올 수 있습니다. user 생성자를 작성했습니다.
  • @OneToMany로 Comment와의 관계를 설정해주었습니다. post에 작성한 댓글 목록을 불러올 수 있습니다.
  • 게시글을 삭제하면 연관된 댓글이 모두 삭제가 되게 cascade = CascadeType.REMOVE 를 붙여주었습니다.

 

Comment

package com.example.post.entity;

import com.example.post.dto.CommentRequestDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@Entity
public class Comment extends Timestamped{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String username;

    @Column(nullable = false)
    private String comment;

    //여러 댓글을 한 게시글에 작성
    @ManyToOne(fetch =FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;

    //여러 댓글을 한명의 user가 작성
    @ManyToOne(fetch =FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    public Comment(String comment, User user, Post post){
        this.username = user.getUsername();
        this.user = user;
        this.comment = comment;
        this.post = post;


    }

    public void update(CommentRequestDto commentRequestDto){
        this.comment = commentRequestDto.getComment();
    }
}
  • @ManytoOne 어노테이션을 넣어서 Post와의 관계를 설정해주었습니다. 한 post에 여러 댓글을 작성할 수 있습니다.
  • @ManytoOne 어노테이션을 넣어서 User와의 관계를 설정해주었습니다. 한명의 user가 여러 댓글을 작성할 수 있습니다.
  • Comment 생성자에 User와 Post를 넣어서 객체로 불러 올 수 있게 작성하였습니다.

 

User

package com.example.post.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

@Getter
@NoArgsConstructor
@Entity(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    @Enumerated(value = EnumType.STRING)
    private UserRoleEnum role;

    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
    private List<Post> posts = new ArrayList<>();

    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
    private List<Comment> comments = new ArrayList<>();


    public User(String username, String password, UserRoleEnum role) {
        this.username = username;
        this.password = password;
        this.role = role;
    }
}
  • @OneToMany로 Comment 및 Post의 관계를 설정해주었습니다. 
  • user를 삭제하면 연관된 댓글 및 게시글이 모두 삭제가 되게 cascade = CascadeType.REMOVE 를 붙여주었습니다.

 

PostResponseDto

package com.example.post.dto;

import com.example.post.entity.Post;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class PostResponseDto {

    private Long id;
    private String title;
    private String content;
    private String username;
    private LocalDateTime createdAt;
    private LocalDateTime modifiedAt;

    private List<CommentResponseDto> commentResponseDtoList = new ArrayList<>();

    //stackoverflow를 방지하기 위해서 객체 직렬화를 해야한다. stream을 써야하나보다?
    public PostResponseDto(Post post){
        this.id = post.getId();
        this.title = post.getTitle();
        this.content = post.getContent();
        this.username = post.getUser().getUsername();
        this.createdAt = post.getCreatedAt();
        this.modifiedAt = post.getModifiedAt();
        this.commentResponseDtoList = post.getCommentLists().stream().map(CommentResponseDto::new).collect(Collectors.toList());

    }
}

 

  • 생성자에 getUser().getUsername()을 사용해서 username을 불러왔습니다.
  • 댓글리스트는 Dto에 담아서 불러옵니다. 바로 가져오면 stackoverflow, 순환 참조 오류가 발생합니다.
  • 이를 해결하기 위해 구글링해보니 dto에 담아오면 되고 목록을 담으려면 stream을 쓰면 된다고 해서 썼는데 여기는 정말 그냥 가져온거라서 나중에 stream에 대해서 학습하고 더 나은 방법이 있나 알아보도록 하겠습니다.

 

CommentResponseDto

package com.example.post.dto;

import com.example.post.entity.Comment;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@NoArgsConstructor
public class CommentResponseDto {

    private Long id;
    private String username;
    private String comment;
    private LocalDateTime createdAt;
    private LocalDateTime modifiedAt;

    private Long postId;

    private Long userId;

    public CommentResponseDto(Comment comment) {
        this.id = comment.getId();
        this.username = comment.getUsername();
        this.comment = comment.getComment();
        this.createdAt = comment.getCreatedAt();
        this.modifiedAt = comment.getModifiedAt();
        this.postId = comment.getPost().getId();
        this.userId = comment.getUser().getId();
    }
}
  • postId는 comment.getPost().getId()를 사용해서 불러왔습니다. 
  • userId는 comment.getUser().getId()를 사용해서 불러왔습니다.
  • JPA 연관관계를 설정했기 때문에 메소드로 호출할 수 있습니다.

 

PostService

  //게시글 전체 조회 USER/ADMIN 상관 없음
    public List<PostResponseDto> getPostList() {
        List<Post> postList = postRepository.findAllByOrderByModifiedAtDesc();

        if(postList.isEmpty()){
            throw new NullPointerException("게시글이 존재하지 않습니다.");
        }

        List<PostResponseDto> postResponseDtoList = new ArrayList<>();
        //각 게시물마다 조회해서 넣어줌
        for (Post post : postList) {
            postResponseDtoList.add(new PostResponseDto(post));
        }
        return postResponseDtoList;
    }

    @Transactional(readOnly = true)
    //게시글 상세 조회 USER/ADMIN 상관 없음
    public PostResponseDto getPost(Long id) {
        Post post = postRepository.findById(id).orElseThrow(
                () -> new NullPointerException("해당 게시글은 존재하지 않습니다.")
        );

//        List<CommentResponseDto> commentResponseDtoList = new ArrayList<>();

//        for (int i =0; i<post.getCommentLists().size(); i++){
//            Comment comment = post.getCommentLists().get(i);
//            commentResponseDtoList.add(new CommentResponseDto(comment));
//        }
        return new PostResponseDto(post);

    }
  • 게시글을 불러오면 댓글은 자동으로 따라오기 때문에 댓글 관련해서 작성했던 코드를 지워주었습니다. 
  • JPA 연관관계 설정을 잘 해놓으니 매우 편리하네요!!

 

 

결론

JPA 연관관계를 도대체 어떻게, 왜 쓰는지를 해결했습니다. 스프링 첫주차부터 JPA 연관관계 강의를 들었는데 이제사 좀 이해가 되는 것 같습니다. 연관관계를 설정해 놓으면 알아서 데이터가 연관되어서 불러오고 삭제하는 것이 간단해집니다. 연관관계 설정도 조금만 더 연습해보면 잘 할 수 있을 것 같습니다. 시간이 괜찮으면 좋아요와 대댓글 기능도 구현을 해보려 합니다.

 

내일부터는 프론트와 함께 하는 첫 협업 프로젝트날입니다😎

Spring과 Java는 사랑입니다😍😍

드디어 댓글 기능을 구현했습니다. 어제는 따로 조회 까지만 했고 오늘 오전에 for문 사용해서 게시글 목록에 댓글까지 전체 조회 하는 기능을 구현했습니다. 이제 이번주차 심화 과제 Lv 1을 조원들과 협력해서 할 예정입니다. Lv1의 경우에는 Spring security를 사용해서 CRUD를 구현하면 됩니다. 시큐리티 부분만 구현하면 나머지는 상대적으로 간단(?)해서 빠르게 구현할 수 있으리라 예상됩니다.

자바 스프링으로 게시판 댓글 구현하기

게시판 댓글 기능 구현

@CommentController

package com.example.post.controller;

import com.example.post.dto.CommentRequestDto;
import com.example.post.dto.CommentResponseDto;
import com.example.post.dto.ResponseDto;
import com.example.post.service.CommentService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RequestMapping("/api/comment/")
@RequiredArgsConstructor
@RestController
public class CommentController {

    private final CommentService commentService;

    @PostMapping("/{postId}")
    public CommentResponseDto createComment(@PathVariable  Long postId, @RequestBody CommentRequestDto requestDto, HttpServletRequest request){
        return commentService.createComment(postId, requestDto, request);
    }

    @PutMapping("/{commentId}")
    public ResponseDto updateComment(@PathVariable Long commentId, @RequestBody CommentRequestDto requestDto, HttpServletRequest request) {
        return commentService.updateComment(commentId, requestDto, request);
    }

    @DeleteMapping("/{commentId}")
    public ResponseDto deleteComment(@PathVariable Long commentId, HttpServletRequest request) {
        return commentService.deleteComment(commentId, request);

    }

}

 

  • 나중에 고쳐보고 싶은 점이라면 url을 /{postId}/{commentId}로 해서 더 직관적으로 만들고 싶습니다. 지금은 POSTMAN으로 테스트하고 있어서 괜찮은데 프론트가 합쳐지면 매우 헷갈릴거 같거든요.
  • 하면서 깨달은 중요한점은 @RequestBody 잊지말자!!!! 입니다. 저 어노테이션을 안달아주면 데이터가 json 형식으로 받질 못해서 에러가 납니다.

 

@CommentService

package com.example.post.service;

import com.example.post.dto.CommentRequestDto;
import com.example.post.dto.CommentResponseDto;
import com.example.post.dto.ResponseDto;
import com.example.post.entity.Comment;
import com.example.post.entity.Post;
import com.example.post.entity.User;
import com.example.post.jwt.JwtUtil;
import com.example.post.repository.CommentRepository;
import com.example.post.repository.PostRepository;
import com.example.post.repository.UserRepository;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CommentService {

    private final JwtUtil jwtUtil;
    private final UserRepository userRepository;
    private final PostRepository postRepository;
    private final CommentRepository commentRepository;

    public CommentResponseDto createComment(Long postId, CommentRequestDto requestDto, HttpServletRequest request) {
        //토큰 가져오기
        String token = jwtUtil.resolveToken(request);
        Claims claims;

        if (token != null) {
            if (jwtUtil.validateToken(token)) {
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            //토큰의 사용자 정보를 사용하여 DB 조회
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            //게시글 조회
            Post post = postRepository.findById(postId).orElseThrow(
                    () -> new NullPointerException("게시글이 존재하지 않습니다.")
            );

            Comment comment = new Comment(user.getUsername(), requestDto.getComment(), post.getId());
            commentRepository.save(comment);

            return new CommentResponseDto(comment);
        } else{
            return null;
        }
    }



    public ResponseDto updateComment(Long commentId, CommentRequestDto requestDto, HttpServletRequest request) {
            //토큰 가져오기
            String token = jwtUtil.resolveToken(request);
            Claims claims;

            if(token != null){
                if(jwtUtil.validateToken(token)) {
                    claims = jwtUtil.getUserInfoFromToken(token);
                }else {
                    throw new IllegalArgumentException("Token Error");
                }

                //토큰의 사용자 정보를 사용하여 DB 조회
                User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                        () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
                );

                Comment comment = commentRepository.findById(commentId).orElseThrow(
                        () -> new NullPointerException("댓글이 존재하지 않습니다.")
                );

                comment.update(requestDto);
                return new ResponseDto(HttpStatus.OK.value(),"댓글 수정 성공");
        }
            else{
                return null;
            }
    }

    public ResponseDto deleteComment(Long commentId, HttpServletRequest request) {
        //토큰 가져오기
        String token = jwtUtil.resolveToken(request);
        Claims claims;

        if(token != null){
            if(jwtUtil.validateToken(token)) {
                claims = jwtUtil.getUserInfoFromToken(token);
            }else {
                throw new IllegalArgumentException("Token Error");
            }

            //토큰의 사용자 정보를 사용하여 DB 조회
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            Comment comment = commentRepository.findById(commentId).orElseThrow(
                    () -> new NullPointerException("댓글이 존재하지 않습니다.")
            );
            commentRepository.deleteById(commentId);
            return new ResponseDto(HttpStatus.OK.value(),"댓글 삭제 성공");

        }else{
            return null;
        }
}
}

 

  • 댓글 작성, 수정, 삭제 자체는 어렵지는 않았습니다. 
  • Dto를 결정할 때, 어떤걸 요청하고 어떤걸 보낼지에 대한 고민은 있었습니다.

 

@PostService

package com.example.post.service;

import com.example.post.dto.CommentResponseDto;
import com.example.post.dto.PostRequestDto;
import com.example.post.dto.PostResponseDto;
import com.example.post.dto.ResponseDto;
import com.example.post.entity.Comment;
import com.example.post.entity.Post;
import com.example.post.entity.User;
import com.example.post.entity.UserRoleEnum;
import com.example.post.jwt.JwtUtil;
import com.example.post.repository.CommentRepository;
import com.example.post.repository.PostRepository;
import com.example.post.repository.UserRepository;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class PostService {

    private final JwtUtil jwtUtil;
    private final PostRepository postRepository;
    private final UserRepository userRepository;

    private final CommentRepository commentRepository;

    //게시글 작성
    @Transactional
    public PostResponseDto createPost(PostRequestDto requestDto, HttpServletRequest request) {
        //토큰 가져오기
        String token = jwtUtil.resolveToken(request);
        Claims claims;

        //토큰 확인 후 게시글 작성 가능
        if(token != null){
            if(jwtUtil.validateToken(token)) {
                claims = jwtUtil.getUserInfoFromToken(token);
            }else {
                throw new IllegalArgumentException("Token Error");
            }

            //토큰의 사용자 정보를 사용하여 DB 조회
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            //요청 받은 Dto로 DB에 저장할 객체 만들기
            Post post = postRepository.saveAndFlush(new Post (requestDto, user.getId(), user.getUsername()));

            return new PostResponseDto(post, null);
        } else {
            return null;
        }

    }

    //게시글 전체 조회 USER/ADMIN 상관 없음
    public List<PostResponseDto> getPostList() {
        List<Post> postList = postRepository.findAllByOrderByModifiedAtDesc();

        if(postList.isEmpty()){
            throw new NullPointerException("게시글이 존재하지 않습니다.");
        }

        List<PostResponseDto> postResponseDtoList = new ArrayList<>();
        //댓글을 각 게시물마다 조회해서 넣어주는건 에바
        for (Post post : postList) {

            List<CommentResponseDto> commentResponseDtoList = new ArrayList<>();
            Long postId = post.getId();
            List<Comment> comments = commentRepository.findAllByPostId(postId);
            for (Comment comment : comments) {
                CommentResponseDto commentResponseDto = new CommentResponseDto(comment);
                commentResponseDtoList.add(commentResponseDto);
            }

            postResponseDtoList.add(new PostResponseDto(post,commentResponseDtoList ));
        }
        return postResponseDtoList;
    }

    @Transactional(readOnly = true)
    //게시글 상세 조회 USER/ADMIN 상관 없음
    public PostResponseDto getPost(Long id) {
        Post post = postRepository.findById(id).orElseThrow(
                () -> new NullPointerException("해당 게시글은 존재하지 않습니다.")
        );

        List<CommentResponseDto> commentResponseDtoList = new ArrayList<>();

        for (int i =0; i<post.getComments().size(); i++){
            Comment comment = post.getComments().get(i);
            commentResponseDtoList.add(new CommentResponseDto(comment));
        }
        return new PostResponseDto(post, commentResponseDtoList);

    }

    @Transactional
    //게시글 수정 USER/ADMIN 권한 설정
    public PostResponseDto updatePost(Long id, PostRequestDto requestDto, HttpServletRequest request) {
        //request에서 token 가져오기
        String token = jwtUtil.resolveToken(request);
        Claims claims;

        //토큰이 있는 경우에만 게시글 수정 가능
        if(token != null){
            if(jwtUtil.validateToken(token)) {
                //토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            //토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            //게시글 있는지 조회
            Post post = postRepository.findByIdAndUserId(id, user.getId()).orElseThrow(
                    () -> new NullPointerException("해당 게시글은 존재하지 않습니다.")
            );


            //사용자 권한 가져와서 Admin 이면 다 수정, USER 이면 본인이 작성한 글만 수정
            UserRoleEnum userRoleEnum = user.getRole();

            if(userRoleEnum == UserRoleEnum.USER) {
                String Username = user.getUsername();
                if(!post.getUsername().equals(Username)){
                    throw new IllegalArgumentException("본인이 작성한 게시글만 수정 가능합니다.");
                }
            }
            post.update(requestDto);
            List<Comment> commentList = commentRepository.findAllByPostId(id);
            List<CommentResponseDto> commentResponseDtoList = new ArrayList<>();

            for (Comment comment : commentList) {
                commentResponseDtoList.add(new CommentResponseDto(comment));
            }
            return new PostResponseDto(post, commentResponseDtoList);
        } else {
            return null;
        }
    }

    @Transactional
    public ResponseDto deletePost(Long id, HttpServletRequest request) {
        //request에서 token 가져오기
        String token = jwtUtil.resolveToken(request);
        Claims claims;

        //토큰이 있는 경우에만 게시글 수정 가능
        if(token != null) {
            if (jwtUtil.validateToken(token)) {
                //토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            //토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            //게시글 있는지 조회
            Post post = postRepository.findByIdAndUserId(id, user.getId()).orElseThrow(
                    () -> new NullPointerException("해당 게시글은 존재하지 않습니다.")
            );


            //사용자 권한 가져와서 Admin 이면 다 수정, USER 이면 본인이 작성한 글만 수정
            UserRoleEnum userRoleEnum = user.getRole();

            if (userRoleEnum == UserRoleEnum.USER) {
                String Username = user.getUsername();
                if (!post.getUsername().equals(Username)) {
                    throw new IllegalArgumentException("본인이 작성한 게시글만 삭제 가능합니다.");
                }
            }
            postRepository.deleteById(id);
            return new ResponseDto(HttpStatus.OK.value(), "게시글 삭제 성공");
        } else {
            return null;
        }
    }
}

 

  • 기존에 구현한 기능에서 조회 기능에 댓글만 추가했습니다.

 

JPA 연관관계

이건 좀 고민 더 해보고 정리해보려고 합니다. 

개인적으로 할 일은 매우 많지만 우선순위를 정해서 포커싱을 하려고 합니다.

 

드디어 댓글기능을 만들어서 게시물 하나에 댓글은 달았는데 전체 조회가 안됩니다. 다른분들한테 물어보니 중첩 for문 써서 돌리라고 하시네요. 여러 예시들을 찾아봤는데 Builder를 사용하는 것이 가장 간지나 보였습니다. 

JPA 연관관계 설정을 하면 테이블을 객체 처럼 참고할 수 있다고 합니다. 그러다가 순환 참조가 걸린다고 하네요.

이제는 새벽 2시에 자는 것을 항상 목표로 하려고 합니다. 

프로그램밍은 시간 가는 지도 모르고 공부를 하게 하네요.

 

더 잘하고 안찾아도 잘하는 개발자가 되고 싶습니다.

 

일단 자바의 정석 아침에 열심히 들어야겠어요.

이번주 목표는 아래와 같습니다.

주간계획

내일은 아침 8시에 일어나서 자바의 정석 꼭 들어야겠습니다. 

ModifedAt CreatedAt null 값으로 나오는 에러

시간이 null 로 나온다

강의 안보고 새로 혼자서 새로 게시판 구현을 하려고 했습니다. 그런데 시간이 null 값으로 떴습니다. 다른분들의 의견은 생성자 주입을 하지 않은 것이 아니냐 였는데, 생성자로 하도 혼이 많이나서 생성자 부터 다 꼼꼼히 만들고 서비스 구현을 시작했던 터라 아무리~ 봐도 생성자 문제는 아니었습니다. 

 

DB 저장 오류

일단 게시글 생성은 시간 빼고는 찍히니까 수정을 해보자 하고 POSTMAN으로 테스트를 해봤는데 아래와 같은 결과가 나왔습니다. 역시나 생성자를 체크했고 결국엔 전에 했던 코드를 가져와서 비교해봤지만 다른 점을 찾기가 어려웠습니다.

불러오는 값 null

@EnableJpaAuditing

각종 구글링 끝에 찾아낸 정답은 아래와 같습니다.

@EnableJpaAuditing

 

application에 @EnableJpaAuditing 어노테이션을 추가하니 아래와 같이 잘 실행되는 것을 확인 할 수 있었습니다.

잘 불러옵니다.

 

@EnableJpaAuditing이란?

Java에서는 ORM 기술인 JPA를 사용하여합니다.

도메인을 관계형 데이터베이스 테이블에 매핑할 때 공통적으로 도메인들이 가지고 있는 필드나 컬럼들이 존재합니다.

대표적으로 createdAt(생성일자), modifiedAt(수정일자), 식별자 같은 필드 및 컬럼이 있습니다.

도메인마다 공통으로 존재한다는 의미는 결국 코드가 중복되고 데이터베이스에서 누가, 언제하였는지 기록을 잘 남겨놓아야 합니다. 

 

JPA에서는 Audit이라는 기능을 제공하고 있다고 합니다. 회계용어로만 알고 있었는데 프로그래밍에도 등장을 하는 단어이네요.

 

Spring Data JPA에서 자동으로 시간값을 넣어주는 기능입니다. 도메인을 영속성 컨텍스트에 저장하거나 조회를 수행한 후에 update를 하는 경우 매번 시간 데이터를 입력하여 주어야 하는데, audit 기능을 이용하면 자동으로 시간을 매핑하여 데이터베이스의 테이블에 넣어줍니다.

 

Timestamped

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class Timestamped {

    @CreatedDate
    private LocalDateTime createdAt;


    @LastModifiedDate
    private LocalDateTime modifiedAt;
}

위의 코드는 제가 시간을 기록하기 위해 만든 클래스 입니다. @MappedSuperclass 와 @EntityListners(AuditingEntityListenr.class) 어노테이션은 잘 넣어놓고 이 기능들을 활성화시키는데 가장 중요한 @EnableJpaAuditing을 까먹은 개발자는 바로 접니다.

 

@MappedSuperclass는 JPA Entity 클래스들이 Timestamped class 를 상속할 경우 createdAt, modifedAt을 칼럼으로 인식하게 합니다.

 

@EntityListners는 문자 그대로 Entity들을 듣는(?) audit하는 기능을 하게 만듭니다.

 

이제는 절대 JPA 사용할 때 @EnableJpaAuditing 을 application 에 아래와 같이 넣는 것을 잊지 맙시다.

 

@EnableJpaAuditing
@SpringBootApplication
public class PostApplication {

    public static void main(String[] args) {
        SpringApplication.run(PostApplication.class, args);
    }

}

 

왜...... 돼?

CRUD 마스터가 되려고 했는데 다른 개인적인 일 때문에 오늘은 집중을 잘 못했습니다. 잠을 별로 못자서 그런거 같기도 합니다. 그래도 Spring security 신경 쓰지말고 숙련주차 CRUD 구현에 집중하라고 해주셔서 한시름 덜었습니다. 지난주차 숙제를 다시 처음부터 복붙없이 써보는 데 왜 안되는지... 왜 저코드를 그대로 보고 썼는데 안되고 복붙하면 되는지는 미스터리입니다. 그래서 코드 비교 웹사이트 활용도 해봤는데 모르겠네요?

https://www.diffchecker.com/

Diffchecker

www.diffchecker.com


결국은 복붙으로 해결하긴 했지만 왜 다른지는 여전히 미스테리입니다.
그냥 따라서 쳤는데... 다른건 스페이스 바 정도 입니다.

근데 왜 안돼...?

스프링 르탄이

어제 새벽 5시에 잤더니 지각했고 생체리듬도 깨진거 같아서 오늘은 일찍 잡니다. 내일 점심에는 오랜만에 운동도 갈 예정입니다.

아마도.

pre-onboarding 주차를 세지 않고 4주차라고 하니까 헷갈려서 그냥 이제부터는 5주차라고 하겠습니다. 이번주도 많은 걸 배웠고 많은 걸 해냈습니다. 복붙의 한계를 느끼고 이제는 제대로 코드를 직접 구현하려고 노력하려고 합니다.

 

되...... 된다!!

항해99에서 함께 하는 조원들이 있어서 감사함을 매일 느끼고 있습니다. 이 부트캠프를 통해 엄청난 개발자가 되는 것은 아니지만 미래의 개발자 주니어들과의 인연은 돈으로 환산할 수 없는 것이라 생각이 들었습니다. 아직은 백엔드끼리 '으쌰으쌰!'라서 나중에 협업을 하게 되면 어떻게 될지는 모르겠지만요. 협업을 하면서 충돌이 많다는 이야기를 듣고 아주 살짝 걱정을 하긴 했습니다. 근데 뭐.. 그래봤자 회사보다는 낫지 않을까요. 극 T 가 된 건 회사생활을 시작하고 나서 인데 백수동안 다시 F쪽으로 기울었다가 다시 개발자를 준비하면서 T가 되었습니다. 감정적으로 말해봤자 변하는 건 없고 빠른 해결책을 구하는 것이 더 효율적입니다. 그런 의미에서 개발 실력을 키우는 게 급선무네요. WIL 얼른 마무리하고 개발하러 가보겠습니다.

 

ORM 이란?

자바의 ORM(Object Relational Mapping) 객체 관계 매핑을 의미합니다. 

객체가 테이블이 되도록 매핑시키는 프레임워크로 프로그램의 복잡도를 줄이고 자바 객체와 쿼리를 분리할 수 있으며 트랜잭션 처리나 기타 데이터베이스 관련 작업들을 더 편리하게 처리할 수 있습니다.

 

ORM기술에 대한 표준 명세가 JPA이고 JPA 표준을 구현한 대표적인 프레임워크가 Hibernate입니다.

 

ORM의 장점:

  • 객체 지향적인 코드로 더 직관적이고 비지니스 로직에 집중 할 수 있게 도와줌
  • 재사용 및 유지 보수 편리성 증대

ORM의 단점:

  • ORM만으로는 다 구현할 수 없고 적절하게 SQL문을 사용할 수 있어야 함
  • 복잡성이 커질수록 난이도가 올라가고 잘못 설계 되었을 경우 속도 저하 및 일관성을 무너뜨리는 문제 발생 가능

 

SQL 이란?

구조적 쿼리 언어(Structured Query Language)는 관계형 데이터베이스에 정보를 저장하고 처리하기 위한 프로그래밍 언어입니다. 관계형 데이터베이스는 정보를 테이블 형식으로 저장하고, 행과 열은 다양한 데이터 속성과 데이터 값 간의 다양한 관계를 나타냅니다. SQL문을 사용하여 데이터베이스에서 정보를 저장, 업데이트, 제거, 검색을 할 수 있습니다. 

 

MySQL 이란?

Oracle에서 제공하는 오픈 소스 관계형 데이터베이스 관리시스템으로 웹 애플리케이션을 위한 데이터베이스 시스템입니다.

SQL 퀴리를 사용하는 관계형 데이터베이스 프로그램입니다.

 

SQL이란 무엇인가요? - SQL - AWS

구조적 쿼리 언어(SQL)는 관계형 데이터베이스에 정보를 저장하고 처리하기 위한 프로그래밍 언어입니다. 관계형 데이터베이스는 정보를 표 형식으로 저장하며, 행과 열은 다양한 데이터 속성과

aws.amazon.com

 

MVC 란? 

MVC (모델-뷰-컨트롤러) 는 사용자 인터페이스, 데이터 및 논리 제어를 구현하는데 널리 사용되는 소프트웨어 디자인 패턴입니다. 소프트웨어의 비즈니스 로직과 화면을 구분하는데 중점을 두고 있습니다. 이러한 "관심사 분리" 는 더나은 업무의 분리와 향상된 관리를 제공합니다.

 

MVC 에 기반을 둔 몇 가지 다른 디자인 패턴으로 MVVM (모델-뷰-뷰모델), MVP (모델-뷰-프리젠터), MVW (모델-뷰-왓에버) 가 있습니다.

 

MVC 소프트웨어 디자인 패턴의 세 가지 부분은 다음과 같이 설명할 수 있습니다.

  1. Model: 데이터와 비즈니스 로직을 관리합니다.
  2. View: 레이아웃과 화면을 처리합니다.
  3. Controller: 명령을 모델과 뷰 부분으로 라우팅합니다. 
 

MVC - 용어 사전 | MDN

MVC (모델-뷰-컨트롤러) 는 사용자 인터페이스, 데이터 및 논리 제어를 구현하는데 널리 사용되는 소프트웨어 디자인 패턴입니다. 소프트웨어의 비즈니스 로직과 화면을 구분하는데 중점을 두고

developer.mozilla.org

 

 

아래는 Controller에 대해 정리해 논 포스팅입니다.

 

스프링 @RestController와 @Controller 차이와 특징

스프링을 사용할 때 스프링 MVC 흐름에 따라 코딩하는데 @Controller를 사용할 때도 있고 @RestController를 사용할 때가 있습니다. 이번 포스팅에서는 언제 @Controller를 사용하고 @RestController를 사용하는

pizzathedeveloper.tistory.com

 

프로그래머스 알고리즘

문제 설명

ROR 게임은 두 팀으로 나누어서 진행하며, 상대 팀 진영을 먼저 파괴하면 이기는 게임입니다. 따라서, 각 팀은 상대 팀 진영에 최대한 빨리 도착하는 것이 유리합니다.

지금부터 당신은 한 팀의 팀원이 되어 게임을 진행하려고 합니다. 다음은 5 x 5 크기의 맵에, 당신의 캐릭터가 (행: 1, 열: 1) 위치에 있고, 상대 팀 진영은 (행: 5, 열: 5) 위치에 있는 경우의 예시입니다.

위 그림에서 검은색 부분은 벽으로 막혀있어 갈 수 없는 길이며, 흰색 부분은 갈 수 있는 길입니다. 캐릭터가 움직일 때는 동, 서, 남, 북 방향으로 한 칸씩 이동하며, 게임 맵을 벗어난 길은 갈 수 없습니다.
아래 예시는 캐릭터가 상대 팀 진영으로 가는 두 가지 방법을 나타내고 있습니다.

  • 첫 번째 방법은 11개의 칸을 지나서 상대 팀 진영에 도착했습니다.

  • 두 번째 방법은 15개의 칸을 지나서 상대팀 진영에 도착했습니다.

위 예시에서는 첫 번째 방법보다 더 빠르게 상대팀 진영에 도착하는 방법은 없으므로, 이 방법이 상대 팀 진영으로 가는 가장 빠른 방법입니다.

만약, 상대 팀이 자신의 팀 진영 주위에 벽을 세워두었다면 상대 팀 진영에 도착하지 못할 수도 있습니다. 예를 들어, 다음과 같은 경우에 당신의 캐릭터는 상대 팀 진영에 도착할 수 없습니다.

게임 맵의 상태 maps가 매개변수로 주어질 때, 캐릭터가 상대 팀 진영에 도착하기 위해서 지나가야 하는 칸의 개수의 최솟값을 return 하도록 solution 함수를 완성해주세요. 단, 상대 팀 진영에 도착할 수 없을 때는 -1을 return 해주세요.

 

제한 사항

  • maps는 n x m 크기의 게임 맵의 상태가 들어있는 2차원 배열로, n과 m은 각각 1 이상 100 이하의 자연수입니다.
    • n과 m은 서로 같을 수도, 다를 수도 있지만, n과 m이 모두 1인 경우는 입력으로 주어지지 않습니다.
  • maps는 0과 1로만 이루어져 있으며, 0은 벽이 있는 자리, 1은 벽이 없는 자리를 나타냅니다.
  • 처음에 캐릭터는 게임 맵의 좌측 상단인 (1, 1) 위치에 있으며, 상대방 진영은 게임 맵의 우측 하단인 (n, m) 위치에 있습니다.

 

입출력 예

 

Maps answer
[[1,0,1,1,1],[1,0,1,0,1],[1,0,1,1,1],[1,1,1,0,1],[0,0,0,0,1]] 11
[[1,0,1,1,1],[1,0,1,0,1],[1,0,1,1,1],[1,1,1,0,0],[0,0,0,0,1]] -1
입출력 예 설명

입출력 예 #1
주어진 데이터는 다음과 같습니다.

캐릭터가 적 팀의 진영까지 이동하는 가장 빠른 길은 다음 그림과 같습니다.

따라서 총 11칸을 캐릭터가 지나갔으므로 11을 return 하면 됩니다.

 

입출력 예 #2
문제의 예시와 같으며, 상대 팀 진영에 도달할 방법이 없습니다. 따라서 -1을 return 합니다.

 

문제 풀이

bfs 를 사용해서 풀어야 합니다. bfs에 대한 개념은 *여기*를 클릭해서 확인하시면 됩니다.

 

솔직히 못풀겠어서 다른 사람들꺼 참고해서 따라해봤습니다.

 

import java.util.LinkedList;
import java.util.Queue;

public class gamemapShortcut {

    //위치를 가지고 있을 객체
    class Node {
        private int row;
        private int col;

        //생성자
        private Node(int row, int col) {
            this.row = row;
            this.col = col;
        }

        //getter
        public int getRow() {
            return row;
        }

        public int getCol() {
            return col;
        }
    }

    public int solution(int[][] maps) {
        Queue<Node> q = new LinkedList<>();
        //row 계산을 위해 상우하좌 순서로 지정
        int[] drow = {0, 1, 0, -1};
        //col 계산을 위해 상우하좌 순서로 지정
        int[] dcol = {-1, 0, 1, 0};
        int row = 0;
        int col = 0;
        //초기값을 큐에 저장
        q.offer(new Node(row, col));
        //큐가 없어질때까지 반복
        while (!q.isEmpty()) {
            Node now = q.poll();
            row = now.getRow();
            col = now.getCol();
            //좌표를 계산
            for (int i = 0; i < 4; i++) {
                int nrow = row + drow[i];
                int ncol = col + dcol[i];
                //다음 좌표가 map 안에 포함되어있고, 다음 위치에 한번도 가지 않았을 경우
                if (
                        nrow >= 0 && nrow < maps[0].length && ncol >= 0 && ncol < maps.length && maps[ncol][nrow] == 1) {
                    //이전까지 온 값을 다음 갈 위치에 누적 합
                    maps[ncol][nrow] = maps[col][row] + 1;
                    //다음 좌표를 큐에 넣음
                    q.offer(new Node(nrow, ncol));
                }
            }
        }
        //maps의 마지막 위치 저장하기
        int answer = maps[maps.length - 1][maps[0].length - 1];
        return answer == 1 ? -1 : answer;
        //마지막 위치에 도착하면 answer 리턴 아니면 -1리턴
    }





    public static void main(String[] args) {
        gamemapShortcut method = new gamemapShortcut();
        int arr[][] = {{1,0,1,1,1},{1,0,1,0,1},{1,0,1,1,1},{1,1,1,0,1},{0,0,0,0,1}};
        System.out.println(method.solution(arr));
    }
}



 

다름 사람 풀이 예시

import java.util.LinkedList;
import java.util.Queue;
import java.awt.Point;
class Solution {
    public static int solution(int[][] maps) {
        int X = maps[0].length;
        int Y = maps.length;
        boolean[][] visited = new boolean[Y][X];
        Queue<Point> q = new LinkedList<Point>();
        int x = 0;
        int y = 0;
        int size = 0;
        int cnt = 0;
        Point p = new Point();
        q.add(new Point(Y-1,X-1));
        while(q.isEmpty()==false) {
            size = q.size();
            cnt++;
            for(int i=0;i<size;i++)
            {
                p = q.peek();
                x = p.y;
                y = p.x;
                q.remove();
                if(visited[y][x]==true)
                    continue;
                maps[y][x] = 0;
                visited[y][x] = true;
                if(x==0 && y==0) {
                    return cnt;
                }
                if(x-1>-1 && maps[y][x-1]==1) { //왼쪽 한칸
                    q.add(new Point(y,x-1));
                }
                if(x+1<X && maps[y][x+1]==1) { //오른쪽 한칸
                    q.add(new Point(y,x+1));
                }
                if(y-1>-1 && maps[y-1][x]==1) { //위쪽 한칸
                    q.add(new Point(y-1,x));
                }
                if(y+1<Y && maps[y+1][x]==1) { //아래쪽 한칸
                    q.add(new Point(y+1,x));
                }
            }
        }
        return -1;
    }

+ Recent posts