멍냥의 전당 좋아요

어제는 오랜만에 일찍 체크아웃을 했습니다. 주특기인 스프링을 배우는 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;
    }

예외 처리 Exception Handling

  • 프로그램에서 예외가 발생했을 경우, 프로그램의 갑작스러운 종료를 막고, 정상 실행을 유지하기 위해 예외 처리를 해야 함
  • 자바 컴파일러는 일반 예외가 발생할 가능성이 있는 코드를 발견하면 컴파일 에러를 발생시켜 예최처리 코드를 개발자가 강제적으로 작성하도록 요구함
  • 실행 예외의 경우에는 개발자의 경험을 바탕으로 예외 처리 코드를 작성해야 함

자바 예외처리

 

예외 처리 코드

  • try-catch-finally 블록은 생성자 내부와 메소드 내부에서 작성되어 일반 예외와 실행 예외가 발생할 경우 예외 처리를 할 수 있도록 해줌

1. try 블록에는 예외 발생 가능 코드가 위치

2. try 블록의 코드가 예외 발생 없이 정상 실행되면 catch 블록의 코드는 실행되지 않고 finally 블록의 코드를 실행;

    try 블록의 코드에서 예외가 발생하면 즉시 실행을 멈추고 catch 블록으로 이동하여 예외 처리 코드 실행 -> finally 블록 코드 실행

3. finally 블록은 생략 가능; 예외 발생 여부와 상관 없이 항상 실행할 내용이 있을 경우에만 finally 블록을 작성함;

    try 블록과 catch 블록에서 return문을 사용하더라도 finally 블록은 항상 실행됨

 

예외 종류에 따른 처리 코드

 

다중 catch

  • 다중 catch 블록을 작성해서 블록5 내부 다양한 예외 처리
  • catch 블록의 예외 클래스 타입은 try 블록에서 발생된 예외의 종류를 말함
  • try 블록에서 해당 타입의 예외가 발생하면 catch 블록을 실행함
  • catch 블록이 여러 개일지라도 단 하나의 catch 블록만 실행 -> 하나의 예외가 발생하면 즉시 실행을 멈추고 해당 catch 블록으로 이동

 

catch 순서

  • 다중 catch 블록을 작성할 때 주의할 점: 상위 예외 클래스가 하위 예외 클래스보다 아래쪽에 위치해야함
  • 하위 예외는 상위 예외를 상속했기 때문에 상위 예외 타입도 되기 때문임

 

예외 떠넘기기

  • 경우에 따라서 메소드를 호출한 곳으로 예외를 떠넘길 수 있음 -> throw 키워드 사용
  • throw 키워드: 메소드 선언부 끝에 작성되어 메소드에서 처리하지 않은 예외를 호출한 곳으로 떠넘기는 역할
  • throw 키워드 뒤에 떠넘길 예외 클래스를 쉼표로 구분해서 나열해주면 됨

 

리턴타입 메소드이름(매개변수, ...) throws 예외클래스1, 예외클래스2, ... {
}

 

 

또는

 

리턴타입 메소드이름(매개변수, ....) throws Exception {
}

 

 

  • throw 키워드가 붙어있는 메소드는 반드시 try 블록 내에서 호출되어야 함
  • catch 블록에서 떠넘겨 받은 예외를 처리해야함

 

public void method1() {

try {

  method2();

} catch(ClassNotFoundException e){

  //예외 처리 코드

  System.out.println("클래스가 존재하지 않습니다.");

  }

}



public void method2() throws ClassNotFoundException {

 Class claszz = class.forName("java.lang.String2");

}



==> 호출한 곳에서 예외 처리 예시

 

 

확인문제

  • try{} 블록에는 예외가 발생할 수 있는 코드를 작성한다
  • catch {} 블록은 try {} 블록에서 발생한 예외를 처리하는 블록이다.
  • try{} 블록에서 return 문을 사용하면 finally {} 블록은 실행되지 않는다. (x)
  • catch{} 블록은 예외의 종류별로 여러 개를 작성할 수 있다.
  •  
  • 생성자나 메소드의 선언 끝부분에 사용되어 내부에서 발생된 예외를 떠넘긴다.
  • throws 뒤에는 떠넘겨야 할 예외를 쉼표(,)로 구분해서 기술한다.
  • 모든 예외를 떠넘기기 위해 간단하게 throws Exception으로 작성할 수 있다.
  • 새로운 예외를 발생시키기 위해 사용된다. (x)

 

 

자바스프링 너무 좋아!!

Spring Security 흐름

  1. 사용자는 회원가입을 진행한다.
    • 해당 URI 요청은 permitAll 처리하고 사용자의 입력값으로 service에서 회원가입을 진행한다.
  2. 사용자의 정보를 저장할 때 비밀번호를 암호화하여 저장한다.
    • PasswordEncoder를 사용하여 비밀번호를 암호화 한 후 저장한다.
  3. 사용자는 로그인을 진행한다.
    • 해당 URI 요청은 permitAll 처리하고 사용자의 입력값으로 service에서 회원 인증을 진행한다. (비밀번호 일치여부 등)
  4. 사용자 인증을 성공하면 사용자의 정보를 사용하여 JWT 토큰을 생성하고 Header에 추가하여 반환한다. Client 는 이를 쿠키저장소에 저장한다.
  5. 사용자는 게시글 작성과 같은 요청을 진행할 때 발급받은 JWT 토큰을 같이 보낸다.
  6. 서버는 JWT 토큰을 검증하고 토큰의 정보를 사용하여 사용자의 인증을 진행해주는 Spring Security 에 등록한 Custom Security Filter 를 사용하여 인증/인가를 처리한다.
  7. Custom Security Filter에서 SecurityContextHolder 에 인증을 완료한 사용자의 상세 정보를 저장하는데 이를 통해 Spring Security 에 인증이 완료 되었다는 것을 알려준다.

 

 OAuth

OAuth 2.0

  • Open Standard for Authorization 의 줄임말로 개방형 Authorization의 표준형
  • JSON형식으로 API 허가 목적으로 개발된  HTTP 기반의 보안 프로토콜
  • 사용하려는 웹사이트 및 애플리케이션에 비밀번호를 제공하지 않고 접근 권한을 부여 받을 수 있게 해주는 공통적 수단
  • 위 그림은 인증/인가의 위임방법을 제공 -> 클라이언트에게 접근토큰(Access Token)을 발급 하는 구조

@Authentication -> HttpSecurity

.authorizeRequests() : 요청에 대한 권한 지정

.anyRequest().authenticated() : 어떠한 요청이든지 인증되어야 함

.formLogin() : 폼을 통한 로그인 이용

.antMatchers.hasRole() 또는 .antMatchers().access() : 해당 경로에 대해 특정 권한을 가져야 접근 가능

 

.antMathcher(). 

 

  • anonymous() : 인증되지 않은 사용자가 접근할 수 있습니다.
  • authenticated() : 인증된 사용자만 접근할 수 있습니다.
  • fullyAuthenticated() : 완전히 인증된 사용자만 접근할 수 있습니다(?)
  • hasRole() or hasAnyRole() : 특정 권한을 가지는 사용자만 접근할 수 있습니다.
  • hasAuthority() or hasAnyAuthority()\ : 특정 권한을 가지는 사용자만 접근할 수 있습니다.
  • hasIpAddress() : 특정 아이피 주소를 가지는 사용자만 접근할 수 있습니다.
  • access() : SpEL 표현식에 의한 결과에 따라 접근할 수 있습니다.
  • not() : 접근 제한 기능을 해제
  • permitAll() or denyAll() : 접근을 전부 허용하거나 제한
  • rememberMe() : 리멤버 기능을 통해 로그인한 사용자만 접근 가능

 

 

[Spring/Security] 초보자가 이해하는 Spring Security - 퍼옴

https://okky.kr/article/382738 # 초보자가 이해하는 Spring Security (좋은 글)저의 스프링 시큐리티 관련 예제는 깃허브 에서 제공합니다. (주석이 포함된 프로젝트는 주석이 너무 지저분하여 제외...)1. 스

postitforhooney.tistory.com

JUnit

단위 테스트 도구;

프로그램을 작은 단위로 쪼개서 각 단위가 정확하게 동작하는지 검사하고 이를 통해 문제 발생시 어느 부분이 잘못되었는지 정확하고 빠르게 확인 할 수 있게 해줌

 

TDD Test-Driven Development

  • 테스트 코드를 먼저 작성하고 실제 동작하는 코드를 개발
  • 설계 -> 테스트 -> 개발 순서로 개발함
  • 단계별로 테스트 코드를 나누어서 작성함
    • Given - 준비
    • When - 실행
    • Then - 검증

 

Mock Object

  • 가짜 객체를 만들어서 분리해서 테스트함
  • Mokito mock을 사용해서 가짜 객체 이용 테스트
    • @Mock 어노테이션으로 Mocking할 객체를 주입 -> 서비스 리포지터리에 가짜 객체가 들어감
    • when() 메서드를 통해 mocking한 객체들이 특정 조건으로 특정 메서드 호출 시 동작하도록 지정

 

AOP Aspect Oriented Programming

  • 핵심기능과 부가기능이 있으면 변경이 쉽게 AOP를 이용해서 부가기능을 모듈화 한다.
  • 부가기능은 핵심기능과 관점(Aspect)가 달라서
  • 핵심기능과 분리해서 부가기능 중심으로 설계 하고 구현
  • 스프링 AOP 어노테이션
    • @Around: '핵심기능' 수행 전과 후 (@Before + @After)
    • @Before: '핵심기능' 호출 전 (ex. Client 의 입력값 Validation 수행)
    • @After: '핵심기능' 수행 성공/실패 여부와 상관없이 언제나 동작 (try, catch 의 finally() 처럼 동작)
    • @AfterReturning: '핵심기능' 호출 성공 시 (함수의 Return 값 사용 가능)
    • @AfterThrowing: '핵심기능' 호출 실패 시. 즉, 예외 (Exception) 가 발생한 경우만 동작 (ex. 예외가 발생했을 때 개발자에게 email 이나 SMS 보냄)

 

 

심화 주차 강의 완강

위의 내용은 강의를 요약만 한것이고 강의는 스프링을 요약의 요약의 요약을 한 것이라서 공부는 더 해야한다.

 

 

 

자바도 모르는데 스프링하는 개발자지망생

분기문 aka 조건문

어떠한 조건에 따라 다른 명령을 실행하게 하는 문법으로, 가장 대표적인 분기문에는 if문이 있습니다.

분기문은 조건문과 같은 의미로 사용됩니다.

 

분기문 (조건문) 종류

java 자바 언어에서 분기문은 if, else if, switch 가 있습니다.

if문은 분기를 처리할 때 가장 많이 쓰이는 문법입니다.

 

조건을 분기하여 처리할 때 사용합니다.

 

분기처리

분기문을 이용해 선택적으로 조건에 따라 코드를 실행하게 만드는 것을 의미합니다.

 

 

 

 

 

CRUD를 제대로 하는 것이 더 중요하지 지금 spring security 좀 덜 이해하고 못해도 이전 주차 것들을 완전히 익혀야 합니다. 

라고 하네요.

security

 

이것도 듣고 저것도 듣는 게 좋다고 생각합니다.

기초부터 차근차근이 되니까 좋네요.

 

속성도 좋지만 가끔은 토대를 쌓는 일도 필요합니다.

 

충고를 가장한 비난은 듣지않습니다. 거부!

 

 

BFS 너비 우선 탐색 Breadth First Search

최단거리 구하는 문제를 풀 때 유용합니다.

 

  • BFS는 시작 지점에서 가까운 노드부터 차례대로 그래프의 모든 노드를 탐색
  • BFS는 같은 레벨에 있는 노드들을 먼저 탐색하고 그다음 레벨 노드를 탐색함
  • 특정 노드에서 다른 노드까지 최단값 또는 임의의 경로를 찾을 때 사용
  • 스택이 아닌 큐(Queeu) 구조를 활용
  • FIFO 원칙으로 탐색

BFS 수행 과정

  1. 현재 레벨에 있는 root Node를 Q.offer에 넣어서 Q.size, 큐의 길이 계산
  2. 큐에서 꺼낸 노드와 인접한 노드들을 모두 방문
  3. 인접한 노드가 없으면 큐의 앞에서 노드를 꺼냄 (dequeue)
  4. 큐에 방문된 노드를 삽입 한다
  5. 큐에 가 공백 상태가 될 때까지 계속 한다 Q.isEmpty() = true 일 때 종료

 

활용 예제

 

Q. 그림과 같은 그래프에 대하여 정점의 개수 N, 연결선의 개수 M이 주어지고 그래프에 대한 연결 정보가 주어질 때, 시작정점 1에서 BFS 방문경로를 출력하시오.

 

그래프 정보

문제 풀이

 

시작 정점 1 bfs() 함수 실행, Queue를 이용하여 

다음 정점과 연결되어 있고 (map[x][i] ==1),

아직 방문하지 않은 정점 (visited[i] == false)

큐에 해당 정점을 넣어주고, 더 이상 방문할 저점이 존재하지 않으면 종료한다.

 

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
import java.util.Queue;
import java.util.LinkedList;

public class bfsprac {
    static int[][] map;
    static boolean[] visited;
    static StringTokenizer st;
    static int N, M, start, end;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        N = Integer.parseInt(br.readLine()); // 정점의 개수
        M = Integer.parseInt(br.readLine()); // 간선의 개수

        map = new int[N+1][N+1];
        visited = new boolean[N+1]; // 방문 여부를 검사할 배열

        for (int m =0; m < M; m++){//그래프 정보 입력받기
            st = new StringTokenizer(br.readLine(), " ");
            start = Integer.parseInt(st.nextToken());
            end = Integer.parseInt(st.nextToken());
            map[start][end] =1;
            map[end][start] =1;
        }
        System.out.print("그래프 BFS 방문 순서 :");
        bfs(1);
    }

    static void bfs(int point) {
        Queue<Integer> queue = new LinkedList<>();
        queue.offer(point);
        visited[point] =true;

        while (!queue.isEmpty()) {
            int x = queue.poll();
            System.out.print(x+" ");

            for (int i =1; i<=N; i++){
                if (map[x][i] ==1 && visited[i]==false){ //다음 정점과 연결되어 있고 아직 방문하지 않았다면
                    queue.offer(i);
                    visited[i]=true;
                }
            }
        }
    }

}

Console

7
8
1 2
1 3
2 4
2 5
3 7
4 6
5 6
6 7
그래프 BFS 방문 순서 :1 2 3 4 5 7 6 

 

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

@Controller

Spring MVC의 컨트롤러는 View를 반환하기 위해 사용합니다. Client의 요청을 받아 View를 반환합니다. Spring MVC의 흐름은 아래와 같습니다:

  1. Client는 URI 형식으로 웹 서비스에 요청
  2. DispatcherServlet은 HandlerMapping에 요청 위임
  3. HandlerMapping을 통해 요청을 Controller로 위임
  4. Controller는 요청을 처리한 후에 ViewName을 반환
  5. DispatcherServlet은 ViewResolver를 통해 ViewName에 해당하는 View를 찾아 클라이언트에게 반환

View를 반환하기 때문에 데이터를 보낼 때는 @RequestBody 어노테이션을 붙여서 Json 형태로 반환해줍니다.

 

@RestController

  • @Controller에 @ResponseBody가 추가된 어노테이션입니다.
  • Json 형태로 객체 데이터를 반환합니다. 
  • @RequestBody를 사용할 필요가 없습니다.
  • REST API 를 개발할 때 주로 사용합니다.

데이터 형식으로 보내는지 반환할 view가 있는지 등을 생각해서 @RestController를 쓸지 @Controller를 사용할 지 선택하고 필요에 따라 @RequestBody를 붙여주면 되겠습니다.

 

 

 

IntelliJ 단축키

Expressions Command + option +v

자동으로 new 생성자 식(Expression)을 만들어 줍니다

 

자동완성 및 줄바꿈 Command + Shift + enter

줄 중간에 있어도 자동으로 괄호 안 완성 해주고 줄바꿈 해줍니다.

 

뒤로 가기 Command + e + enter

바로 전 코드 줄로 이동합니다.

 

폴더 목록으로 이동 Command +1

폴더 목록으로 이동 합니다.

 

기타 IntelliJ 🍯팁

- 북마크 설정해서 자주 보는 코드 또는 일하고 있던 코드 바로 가기가 가능합니다.

- //TODO 를 입력해도 마크가 되서 바로가기 할 수 있습니다. 작업하던 곳을 헷갈리지 않고 찾아갑니다.

 

BONUS 🤓 Spring 관련 인프런 강의 조언

- 김영한 Spring 무료 강의 + 핵심편

- ORM 표준 JPA 기본편

- Spring Web MVC 1편 (2편은 갠적으로 비추하심)

 

  • 예외(exception): 사용자의 잘못된 조작 또는 개발자의 잘못된 코딩으로 인해 발생하는 프로그램 오류
  • 예외 처리(exception handling): 프로그램을 종료하지 않고 정상 실행 상태가 유지되도록 할 수 있음
  • 자바는 예외 처리 유무를 확인하고 예외 처리 코드가 없다면 컴파일이 안됨

 

예외와 예외 클래스

  • 일반 예외: exception
  • 실행 예외:runtime exception
  • RuntimeException 클래스 기준으로 구분

 

실행 예외

  • 자바 컴파일러가 체크하지 않기 때문에 개발자가 예외 처리 코드를 작성해야 함

 

NullPointerException

  • 객체 참조가 없는 상태, 즉 null 값을 갖는 참조 변수로 객체 접근 연산자인 도트(.)를 사용했을 때 발생
  • 객체가 없는 상태에서 객체를 사용하려고 할때 생기는 예외

 

ArrayIndexOutOfBoundsException

  • 배열에서 인텍스 범위를 초과할 경우 생기는 실행 예외

 

NumberFormantException

  • 문자열을 숫자로 변환 할 때 숫자로 변환될 수 없는 문자가 포함되어 있을 때 발생

 

ClassCastException

  • 타입변환은 상위클래스와 하위 클래스 간에 발생하고 구현 클래스와 인터페이스 간에 발생하는데 이런 관계가 아닐 때 발생하는 예외
  • instanceof 연산자 사용해서 타입 변환 전에 변환 가능한지 확인하기

 

확인문제

  • 예외는 사용자의 잘못된 조작 개발자의 잘못된 코딩으로 인한 프로그램 오류를 말한다.
  • RuntimeException의 하위 클래스는 컴파일러가 예외처리 코드를 체크하지 않는다.
  • 예외는 클래스로 관리된다.
  • Exception의 하위 클래스는 모두 일반 예외에 해당한다. (x)

자바 혼공자 익명객체

익명 객체

  • 익명(anonymous) 객체: 이름이 없는 객체
  • 어떤 클래스를 상속하거나 인터페이스를 구현해야 만들 수 있음
[상속]
부모클래스 변수 = new 부모클래스(){....};

[구현]
인터페이스 변수 = new 인터페이스() {....};

 

 

익명 자식 객체 생성

  • 자식 클래스가 재사용되지 않고 오직 특정 위치에서 사용할 경우 -> 익명 자식 객체 사용

 

부모클래스 [필드|변수] = new 부모클래스(매개값, ...){

  //필드

  //메소드

};
  • 익명 자식 객체에 새로 정의된 필드와 메소드는 익명 자식 객체 내부에서만 사용하고 외부에서는 접근 x
  • 익명 자식 객체는 부모 타입 변수에 대입되므로 부모 타입에 선언된 것만 사용 가능

 

익명 구현 객체 생성

  • 구현 클래스가 재사용 되지 않고 오직 특정 위치에서 사용하는 경우, 익명 구현 객체 사용
인터페이스[필드|변수] = new 인터페이스(){ //인터페이스에 선언된 추상 메소드의 실제 메소드 선언 
	//필드 
	//메소드
};

 

 

+ Recent posts