Spring 숙련 주차 개인 과제 LV1

박영준·2022년 12월 7일
0

Java

목록 보기
28/112

요구사항

추가된 요구사항

  1. 회원 가입 API
    • username, password를 Client에서 전달받기
    • username은 최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z), 숫자(0~9)로 구성되어야 한다.
    • password는 최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9)로 구성되어야 한다.
    • DB에 중복된 username이 없다면 회원을 저장하고 Client 로 성공했다는 메시지, 상태코드 반환하기
  2. 로그인 API
    • username, password를 Client에서 전달받기
    • DB에서 username을 사용하여 저장된 회원의 유무를 확인하고 있다면 password 비교하기
    • 로그인 성공 시, 로그인에 성공한 유저의 정보와 JWT를 활용하여 토큰을 발급하고,
      발급한 토큰을 Header에 추가하고 성공했다는 메시지, 상태코드 와 함께 Client에 반환하기

전체 요구사항

  1. 전체 게시글 목록 조회 API
    • 제목, 작성자명(username), 작성 내용, 작성 날짜를 조회하기
    • 작성 날짜 기준 내림차순으로 정렬하기
  2. 게시글 작성 API
    • 토큰을 검사하여, 유효한 토큰일 경우에만 게시글 작성 가능
    • 제목, 작성자명(username), 작성 내용을 저장하고
    • 저장된 게시글을 Client 로 반환하기
  3. 선택한 게시글 조회 API
    • 선택한 게시글의 제목, 작성자명(username), 작성 날짜, 작성 내용을 조회하기
      (검색 기능이 아닙니다. 간단한 게시글 조회만 구현해주세요.)
  4. 선택한 게시글 수정 API
    • 수정을 요청할 때 수정할 데이터와 비밀번호를 같이 보내서 서버에서 비밀번호 일치 여부를 확인 한 후
    • 토큰을 검사한 후, 유효한 토큰이면서 해당 사용자가 작성한 게시글만 수정 가능
    • 제목, 작성 내용을 수정하고 수정된 게시글을 Client 로 반환하기
  5. 선택한 게시글 삭제 API
    • 삭제를 요청할 때 비밀번호를 같이 보내서 서버에서 비밀번호 일치 여부를 확인 한 후
    • 토큰을 검사한 후, 유효한 토큰이면서 해당 사용자가 작성한 게시글만 삭제 가능
    • 선택한 게시글을 삭제하고 Client 로 성공했다는 메시지, 상태코드 반환하기

API

ERD

질문 & 답변

  1. 처음 설계한 API 명세서에 변경사항이 있었나요?
    변경 되었다면 어떤 점 때문 일까요? 첫 설계의 중요성에 대해 작성해 주세요!
  2. ERD를 먼저 설계한 후 Entity를 개발했을 때 어떤 점이 도움이 되셨나요?
  3. JWT를 사용하여 인증/인가를 구현 했을 때의 장점은 무엇일까요?
  4. 반대로 JWT를 사용한 인증/인가의 한계점은 무엇일까요?
  5. 만약 댓글 기능이 있는 블로그에서 댓글이 달려있는 게시글을 삭제하려고 한다면 무슨 문제가 발생할까요? Database 테이블 관점에서 해결방법이 무엇일까요?
  6. IoC / DI 에 대해 간략하게 설명해 주세요!

전체적인 패키지 및 파일 생성


controller

BoardController

package com.sparta.springboard.controller;

import com.sparta.springboard.dto.BoardRequestDto;
import com.sparta.springboard.dto.BoardResponseDto;
import com.sparta.springboard.dto.MsgResponseDto;
import com.sparta.springboard.service.BoardService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

//Controller 클래스(여기서는 BoardController)의 각 하위 메서드에 @ResponseBody 를 붙이지 않아도(@Controller 와 달리), 문자열과 JSON 등을 전송 가능
//RestController 의 주용도는 Json 형태로 객체 데이터를 반환하는 것
//@Controller + @ResponseBody 를 결합한 어노테이션
@RestController

//final 또는 @NotNull 이 붙은 필드의 생성자를 자동 생성. 주로 의존성 주입(Dependency Injection) 편의성을 위해서 사용
@RequiredArgsConstructor

//url 에서 공통되는 부분
@RequestMapping("/api")

public class BoardController {

    //HTTP request 를 받아서, Service 쪽으로 넘겨주고, 가져온 데이터들을 requestDto 파라미터로 보냄
    private final BoardService boardService;

    //게시글 작성
    @PostMapping("/post")
    //BoardResponseDto 반환 타입, createBoard 메소드 명
    //@RequestBody: HTTP Method 안의 body 값을 Mapping(key:value 로 짝지어줌), BoardRequestDto: 넘어오는 데이터를 받아주는 객체
    //HttpServletRequest request 객체: 누가 로그인 했는지 알기위한 토큰을 담고 있음
    public BoardResponseDto createBoard(@RequestBody BoardRequestDto requestDto, HttpServletRequest request) {
        //requestDto, request 에 데이터를 담아서, boardService 로 응답을 보냄
        return boardService.createBoard(requestDto, request);
    }

    //전체 게시글 목록 조회
    @GetMapping("/posts")
    //BoardResponseDto 를 List 로 반환하는 타입, getListBoards 메소드 명, () 전부 Client 에게로 반환하므로 비워둠
    public List<BoardResponseDto> getListBoards() {
        //() 모든 데이터를 담아서, boardService 로 응답을 보냄
        return boardService.getListBoards();
    }

    //선택한 게시글 조회
    @GetMapping("/post/{id}")
    //BoardResponseDto 반환 타입, getBoards 메소드 명
    //@PathVariable: URL 경로에 변수를 넣기, Long id: 담을 데이터 --> 전체 게시글 목록에서 id 값으로 각각의 게시글을 구별
    public BoardResponseDto getBoards(@PathVariable Long id) {
        //id 값을 담아서, boardService 로 응답을 보냄
        return boardService.getBoard(id);
    }

    //선택한 게시글 수정(변경)
    @PutMapping("/post/{id}")
    //BoardResponseDto 반환 타입, updateBoard 메소드 명
    //@PathVariable: URL 경로에 변수를 넣기, Long id: 담을 데이터,
    //@RequestBody: HTTP Method 안의 body 값을 Mapping(key:value 로 짝지어줌), BoardRequestDto: 넘어오는 데이터를 받아주는 객체
    //HttpServletRequest request 객체: 누가 로그인 했는지 알기위한 토큰을 담고 있음
    public BoardResponseDto updateBoard(@PathVariable Long id, @RequestBody BoardRequestDto requestDto, HttpServletRequest request) {
        //id 값, requestDto, request 에 데이터를 담아서, boardService 로 응답을 보냄
        return boardService.updateBoard(id, requestDto, request);
    }

    //선택한 게시글 삭제
    @DeleteMapping("/post/{id}")
    //MsgResponseDto 반환 타입, deleteBoard 메소드 명
    //@PathVariable: URL 경로에 변수를 넣기, Long id: 담을 데이터, HttpServletRequest request 객체: 누가 로그인 했는지 알기위한 토큰을 담고 있음
    public MsgResponseDto deleteBoard(@PathVariable Long id, HttpServletRequest request) {
        //id 값, request 에 데이터를 담아서, boardService 로 응답을 보냄
        return boardService.deleteBoard(id, request);
    }
}

UserController

package com.sparta.springboard.controller;

import com.sparta.springboard.dto.LoginRequestDto;
import com.sparta.springboard.dto.MsgResponseDto;
import com.sparta.springboard.dto.SignupRequestDto;
import com.sparta.springboard.service.UserService;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

//Client 요청으로부터 view 를 반환. MVC 패턴의 Controller 클래스임을 명시
@Controller

//final 또는 @NotNull 이 붙은 필드의 생성자를 자동 생성. 주로 의존성 주입(Dependency Injection) 편의성을 위해서 사용
@RequiredArgsConstructor

//url 에서 공통되는 부분
@RequestMapping("/api/auth")

public class UserController {

    //HTTP request 를 받아서, Service 쪽으로 넘겨주고, 가져온 데이터들을 requestDto 파라미터로 보냄
    private final UserService userService;

    //회원가입 구현
    @PostMapping("/signup")
    //ResponseEntity: 결과값, 상태코드, 헤더값을 모두 프론트에 넘겨줄 수 있고, 에러코드 또한 섬세하게 설정해서 보내줄 수 있음 --> 구글링 필요, MsgResponseDto 의 데이터를 반환할 것임, signup 메소드 명
    //@RequestBody: HTTP Method 안의 body 값을 Mapping(key:value 로 짝지어줌), SignupRequestDto: 넘어오는 데이터를 받아주는 객체
    //@Valid: Controller 에서 유효성 검사를 할 곳에 붙임
    public ResponseEntity<MsgResponseDto> signup(@RequestBody @Valid SignupRequestDto signupRequestDto) {
        //signupRequestDto 에 데이터를 담아서, userService 로 응답을 보냄
        userService.signup(signupRequestDto);
        //MsgResponseDto 에서 선언한 타입(여기서는 String message, int statusCode)으로 반환하는데,
        //ResponseEntity.ok(): 상태코드를 반환
        return ResponseEntity.ok(new MsgResponseDto("회원가입 완료", HttpStatus.OK.value()));
    }

    //로그인 구현
    @ResponseBody
    @PostMapping("/login")
    //ResponseEntity: 결과값, 상태코드, 헤더값을 모두 프론트에 넘겨줄 수 있고, 에러코드 또한 섬세하게 설정해서 보내줄 수 있음 --> 구글링 필요, MsgResponseDto 의 데이터를 반환할 것임, login 메소드 명
    //@RequestBody: HTTP Method 안의 body 값을 Mapping(key:value 로 짝지어줌), LoginRequestDto: 넘어오는 데이터를 받아주는 객체
    //HttpServletRequest request 객체: 누가 로그인 했는지 알기위한 토큰을 담고 있음
    public ResponseEntity<MsgResponseDto> login(@RequestBody LoginRequestDto loginRequestDto, HttpServletResponse response) {
        //loginRequestDto, response 에 데이터를 담아서, userService 로 응답을 보냄
        userService.login(loginRequestDto, response);
        //MsgResponseDto 에서 선언한 타입(여기서는 String message, int statusCode)으로 반환하는데,
        //ResponseEntity.ok(): 상태코드를 반환
        return ResponseEntity.ok(new MsgResponseDto("로그인 완료",HttpStatus.OK.value()));
    }
}

dto

BoardRequestDto

package com.sparta.springboard.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDateTime;

//@Getter, @Setter: 필드에 선언시 자동으로 get, set 메소드 생성. 클래스에서 선언시 모든 필드에 접근자와 설정자가 자동으로 생성
@Getter
@Setter

//파라미터가 없는 기본생성자를 생성
@NoArgsConstructor

public class BoardRequestDto {
    
    //필드
    private Long id;
    private String title;
    private String contents;
    private String username;
    private LocalDateTime createdAt;
    private LocalDateTime modifiedAt;

    private String password;
    private String role;
}

BoardResponseDto

package com.sparta.springboard.dto;

import com.sparta.springboard.entity.Board;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

//@Getter, @Setter: 필드에 선언시 자동으로 get, set 메소드 생성. 클래스에서 선언시 모든 필드에 접근자와 설정자가 자동으로 생성
@Getter

//파라미터가 없는 기본생성자를 생성
@NoArgsConstructor

public class BoardResponseDto {

    //필드
    private Long id;
    private String title;
    private String contents;
    private String username;
    private LocalDateTime createdAt;
    private LocalDateTime modifiedAt;
    
    //생성자
    public BoardResponseDto(Board board) {      //매개변수를 가지는 생성자
        this.id = board.getId();            //this.id: (위에서 선언된) 필드, Board 객체의 board 매개변수로 들어온 데이터를 getId() 에 담는다(Client 에게로 보내기 위해)
        this.title = board.getTitle();
        this.contents = board.getContents();
        this.username = board.getUsername();
        this.createdAt = board.getCreatedAt();
        this.modifiedAt = board.getModifiedAt();

    }
}

LoginRequestDto

package com.sparta.springboard.dto;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter

public class LoginRequestDto {
    
    //필드
    private String username;
    private String password;
}

MsgResponseDto

package com.sparta.springboard.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

//@Getter, @Setter: 필드에 선언시 자동으로 get, set 메소드 생성. 클래스에서 선언시 모든 필드에 접근자와 설정자가 자동으로 생성
@Getter

//모든 필드값을 파라미터로 받는 생성자 생성
@AllArgsConstructor

//파라미터가 없는 기본생성자를 생성
@NoArgsConstructor

//해당 클래스에 자동으로 빌더를 추가
@Builder

public class MsgResponseDto {

    //필드
    private String msg;          //BoardResponseDto 에 있던 필드와 생성자를 별도로 ResponseDto 를 만들어서 분리해줄 수도 있다!
    private int statusCode;             //기능에 따라서 반환하려는 데이터가 다른 경우에!
    
    //@AllArgsConstructor 덕분에, 모든 필드값을 파라미터로 받는 생성자를 굳이 생성해주지 않아도 됨
//    public MsgResponseDto(String msg, int statusCode) { 
//        this.msg = msg;
//        this.statusCode = statusCode;
//    }
}

SignupRequestDto

package com.sparta.springboard.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
//@NoArgsConstructor ??
@AllArgsConstructor         //추가?
@Builder      //추가?
public class SignupRequestDto {
    
    //필드
    private String username;
    private String password;
    private boolean admin = false;
    private String adminToken = "";
}

entity

Board

package com.sparta.springboard.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.sparta.springboard.dto.BoardRequestDto;
import jakarta.persistence.*;
//import javax.persistence.*; ??
import lombok.Getter;
import lombok.NoArgsConstructor;

//@Getter, @Setter: 필드에 선언시 자동으로 get, set 메소드 생성. 클래스에서 선언시 모든 필드에 접근자와 설정자가 자동으로 생성
@Getter
@Entity

//파라미터가 없는 기본생성자를 생성
@NoArgsConstructor

public class Board extends Timestamped {

    //PK(Primary Key: DB 테이블의 유일한 값)임을 지정해줌. 특정 속성을 기본키로 설정
    //@Id 어노테이션만 적게될 경우 기본키값을 직접 부여해줘야 함 --> 그래서 @GeneratedValue 를 사용
    @Id
    //@GeneratedValue(strategy = GenerationType.IDENTITY) 또는 SEQUENCE) 또는 TABLE) 또는 AUTO)
        //각각의 기능: 기본 키 생성을 DB에 위임 (Mysql), DB 시퀀스를 사용해서 기본 키 할당 (ORACLE), 키 생성 테이블 사용 (모든 DB 사용 가능), 선택된 DB에 따라 자동으로 전략 선택
            //AUTO: DB에 따라 전략을 JPA 가 자동으로 선택되므로, DB를 변경해도 코드를 수정할 필요 없다는 장점
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    //객체 필드를 테이블 컬럼과 매핑 + 여러 속성 설정 가능
        //nullable: null 허용 여부
    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String username;

    @Column(nullable = false)
    private String contents;

    //필드 레벨에 적용되어 해당 필드를 Jackson 이 무시할 수 있도록 함(필드 하나하나에 붙임)
    @JsonIgnore                 //왜 붙은걸까?
    @Column
    private String password;

    @Column(nullable = false)
    private Long userId;


    //생성자
    //게시글 작성
    public Board(BoardRequestDto requestDto, Long userId) {
        this.title = requestDto.getTitle();             //this.title: (위에서 선언된) 필드, BoardRequestDto 객체의 requestDto 매개변수로 들어온 데이터를 getTitle() 에 담는다(DB 로 보내기 위해)
        this.username = requestDto.getUsername();
        this.contents = requestDto.getContents();
        this.password = requestDto.getPassword();
        this.userId = userId;                        //userId: 다른 값과 일치하는지를 비교해서 본인 인증을 위해 (연관관계를 짓기 위함)?
    }

    //선택한 게시글 수정(변경)
    public void update(BoardRequestDto requestDto) {       //boardrequestDto? requestDto?
        this.title = requestDto.getTitle();             //this.title: (위에서 선언된) 필드, BoardRequestDto 객체의 requestDto 매개변수로 들어온 데이터를 getTitle() 에 담는다(DB 로 보내기 위해)
        this.username = requestDto.getUsername();
        this.contents = requestDto.getContents();
        this.password = requestDto.getPassword();
    }
}

Timestamped

package com.sparta.springboard.entity;

import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

//@Getter, @Setter: 필드에 선언시 자동으로 get, set 메소드 생성. 클래스에서 선언시 모든 필드에 접근자와 설정자가 자동으로 생성
@Getter

//객체의 입장에서 공통 매핑 정보가 필요할 때 사용
//공통 매핑 정보가 필요할 때, 부모 클래스에 선언하고, 속성만 상속 받아서 사용하고 싶을 때 --> Board 가 Timestamped 를 상속받고있다!
@MappedSuperclass

//Entity 가 삽입, 삭제, 수정, 조회 등...의 작업을 하기 전/후에 어떤 작업을 하기 위해 이벤트 처리를 위해서
    //예시: 데이터 삽입 전 또는 수정 전에 createdAt 과 modifiedAt 의 시간을 조작
//@EntityListeners(AuditingEntityListener.class) : 해당 클래스에 Auditing 기능(시간에 대해서 자동으로 값을 넣어주는 기능)을 포함
@EntityListeners(AuditingEntityListener.class)

public class Timestamped {
    //createdAt, modifiedAt 컬럼 2개를 가진다

    //생성일자
    @CreatedDate
    private LocalDateTime createdAt;

    //마지막 수정일자
    @LastModifiedDate
    private LocalDateTime modifiedAt;
}

User

package com.sparta.springboard.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.NoArgsConstructor;

//@Getter, @Setter: 필드에 선언시 자동으로 get, set 메소드 생성. 클래스에서 선언시 모든 필드에 접근자와 설정자가 자동으로 생성
@Getter

//파라미터가 없는 기본생성자를 생성
@NoArgsConstructor

//DB 테이블 역할을 함
//JPA 를 사용해 테이블과 매핑할 클래스에 붙임
//기본 생성자 필수
//name 속성: JPA 에서 사용할 엔티티 이름 지정. 따로 지정하지 않을 시 기본값으로 클래스 이름을 그대로 Entity 이름으로 지정
@Entity(name = "users")
public class User {

    //필드
    
    //PK(Primary Key: DB 테이블의 유일한 값)임을 지정해줌. 특정 속성을 기본키로 설정
    //@Id 어노테이션만 적게될 경우 기본키값을 직접 부여해줘야 함 --> 그래서 @GeneratedValue 를 사용
    @Id
    //@GeneratedValue(strategy = GenerationType.IDENTITY) 또는 SEQUENCE) 또는 TABLE) 또는 AUTO)
        //각각의 기능: 기본 키 생성을 DB에 위임 (Mysql), DB 시퀀스를 사용해서 기본 키 할당 (ORACLE), 키 생성 테이블 사용 (모든 DB 사용 가능), 선택된 DB에 따라 자동으로 전략 선택
            //AUTO: DB에 따라 전략을 JPA 가 자동으로 선택되므로, DB를 변경해도 코드를 수정할 필요 없다는 장점
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    //객체 필드를 테이블 컬럼과 매핑 + 여러 속성 설정 가능
        //nullable: null 허용 여부
        //unique: 중복 허용 여부 (false 일때 중복 허용)
    @Column(nullable = false, unique = true)
    //문자열, 배열등의 크기 설정
    @Size(min = 4,max = 10,message ="아이디는 4에서 10자 사이 입니다.")
    //정규식 설정
    @Pattern(regexp = "[a-z0-9]*$",message = "아이디 형식이 일치하지 않습니다.")
    private String username;

    @Column(nullable = false)
    @Size(min = 8,max = 15,message ="비밀번호는 8에서 15자 사이 입니다.")
    @Pattern(regexp = "[a-zA-Z0-9`~!@#$%^&*()_=+|{};:,.<>/?]*$",message = "비밀번호 형식이 일치하지 않습니다.")
    private String password;

    @Column(nullable = false)
    //Enum 의 선언된 상수의 이름을 String 클래스 타입으로 변환하여 DB에 넣는다.(즉, DB 클래스 타입은 String 이다.) --> UserRoleEnum 에서 열거혐(enum) 으로 필드가 선언되어있음
    @Enumerated(value = EnumType.STRING)
    private UserRoleEnum role;

    //생성자
    //필드명 앞에 this. 를 붙임으로써
        //1. 객체 자기 자신을 참조한다고 밝히고,
        //2. 매개변수이름과 똑같았던 필드명을 구분지어줘서 필드에 접근 가능하도록 만듦(원래는 매개변수의 우선순위가 더 높았기때문에)
    public User(String username, String password, UserRoleEnum role) {
        this.username = username;       //this.username: (위에서 선언된) 필드, username: 매개변수
        this.password = password;
        this.role = role;
    }
}

UserRoleEnum

package com.sparta.springboard.entity;

//주의! 열거혐(enum)으로 만들어야 함
public enum UserRoleEnum {
    USER,  // 사용자 권한
    ADMIN  // 관리자 권한
}

jwt

JwtUtil

package com.sparta.springboard.jwt;

import com.sparta.springboard.entity.UserRoleEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.security.Key;
import java.util.Base64;
import java.util.Date;

//@Slf4j: 로깅 추상화 라이브러리. 변수명이 log 로 고정된다
    //log 가 왜 필요한가?  로그를 작성해두면, 어떤 동작 중인지, 어느 부분에 에러가 났는지 파악 가능
@Slf4j

//Bean Configuration 파일에 Bean 을 따로 등록하지 않아도 사용 가능해짐
//빈 등록을 위해
@Component

//final 또는 @NotNull 이 붙은 필드의 생성자를 자동 생성. 주로 의존성 주입(Dependency Injection) 편의성을 위해서 사용
@RequiredArgsConstructor

public class JwtUtil {  //빈이 등록됐다는 '나뭇잎 모양' 확인 가능

//필드
    
//토큰 생성에 필요한 값

    //Authorization: Bearer <JWT> 에서 Header 에 들어가는 KEY 값
    public static final String AUTHORIZATION_HEADER = "Authorization";

    // 사용자 권한 값의 KEY: 사용자 권한도 Token 안에 넣는데, 이를 가져올 때 사용하는 KEY 값
    public static final String AUTHORIZATION_KEY = "auth";

    // Token 식별자: Token 을 만들 때, 앞에 들어가는 부분
    private static final String BEARER_PREFIX = "Bearer ";

    // 토큰 만료시간: 밀리세컨드 기준. 60 * 1000L는 1분. 60 * 60 * 1000L는 1시간
    private static final long TOKEN_TIME = 60 * 60 * 1000L;

    //@Value("${프로퍼티 키값}") : application.properties 에 정의한 내용을 가져와서 사용 가능
        //수정과 관리가 용이하기 때문에 이렇게 사용
    @Value("${jwt.secret.key}")
    private String secretKey;   //@Value() 안에 application.properties 에 넣어둔 KEY 값(jwt.secret.key=7ZWt7ZW0O...pA=)을 넣으면, 가져올 수 있음
    private Key key;    //Key 객체: Token 을 만들 때 넣어줄 KEY 값
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;


//메소드
    //종속성 주입이 완료된 후 실행되어야 하는 메서드에 사용
    //처음에 객체가 생성될 때, 초기화하는 함수
        //사용 이유?
            //1. 생성자가 호출되었을 때, 빈은 초기화되지 않았음(의존성 주입이 이루어지지 않았음)
            //2. 어플리케이션이 실행될 때 bean 이 오직 한 번만 수행되게 해서  bean 이 여러 번 초기화되는 걸 방지
    @PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);   //Base64로 인코딩되어 있는 것을, 값을 가져와서(getDecoder()) 디코드하고(decode(secretKey)), byte 배열로 반환
        key = Keys.hmacShaKeyFor(bytes);    //반환된 bytes 를 hmacShaKeyFor() 메서드를 사용해서 Key 객체에 넣기
    }

//Header 에서 Token 가져오기
    public String resolveToken(HttpServletRequest request) {
        //HttpServletRequest request 객체 안에 들어간 Token 값을 가져옴
        //() 안에는 어떤 KEY 를 가져올지 정할 수 있음(여기선 AUTHORIZATION_HEADER 안에 있는 KEY 의 값을 가져옴)
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        //그리고, 가져온 코드가 있는지, BEARER_PREFIX 로 시작하는지 확인
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
            //substring() 메소드를 사용해서, 앞의 일곱 글자를 지워줌 --> 앞의 일곱 글자는 Token 과 관련 없는 String 값이므로
            return bearerToken.substring(7);
        }
        return null;
    }

//JWT 생성
    public String createToken(String username, UserRoleEnum role) {
        Date date = new Date();

        return BEARER_PREFIX +      //Token 앞에 들어가는 부분임
                //실제로 만들어지는 부분
                Jwts.builder()
                        //setSubject 라는 공간 안에 username 를 넣는다
                        .setSubject(username)
                        //claim 이라는 공간 안에 AUTHORIZATION_KEY 사용자의 권한을 넣고(이 권한을 가져올 때는 지정해둔 auth KEY 값을 사용해서 넣음)
                        .claim(AUTHORIZATION_KEY, role)
                        //이 Token 을 언제까지 유효하게 가져갈건지. date: 위의 Date date = new Date() 에서 가져온 부분
                        //getTime(): 현재 시간
                        //+ TOKEN_TIME: 우리가 지정해 둔 시간(TOKEN_TIME = 60 * 60 * 1000L)을 더한다 = 즉, 지금 기준으로 언제까지 가능한지 결정해줌
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME))
                        //Token 이 언제 만들어졌는지 넣는 부분
                        .setIssuedAt(date)
                        //secret key 를 사용해서 만든 key 객체와
                        //어떤 암호화 알고리즘을 사용해서 암호화할것인지 지정(SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256)
                        .signWith(key, signatureAlgorithm)
                        //반환
                        .compact();
    }

//JWT 검증
    public boolean validateToken(String token) {
        try {
            //Jwts: 위에서 JWT 생성 시 사용했던 것
            //parserBuilder(): 검증 방법
            //setSigningKey(key): Token 을 만들 때 사용한 KEY
            //parseClaimsJws(token): 어떤 Token 을 검증할 것인지
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);  //이 코드만으로 내부적으로 검증 가능
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT token, 만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
        } catch (IllegalArgumentException e) {
            log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
        }
        return false;
    }


//JWT 에서 사용자 정보 가져오기
    // 토큰에서 사용자 정보 가져오기 --> 위에서 validateToken() 으로 토큰을 검증했기에 이 코드를 사용할 수 있는 것
    public Claims getUserInfoFromToken(String token) {
        //getBody(): Token?? 안에 들어있는 정보를 가져옴
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }
}

repository

BoardRepository

package com.sparta.springboard.repository;

import com.sparta.springboard.entity.Board;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.Optional;

//게시글 작성
//<연결할 엔티티(테이블) 클래스 이름, ID 필드 타입> --> Board 에서 id 필드의 타입이 Long 이기 때문
public interface BoardRepository extends JpaRepository<Board, Long> {

//전체 게시글 목록 조회
    List<Board> findAllByOrderByModifiedAtDesc();

//게시글 수정, 삭제
    //Optional: null 을 반환하면 오류가 발생할 가능성이 매우 높은 경우에 '결과 없음'을 명확하게 드러내기 위해
        //--> 수정, 삭제에서 id 와 userid 를 비교해서 일치하지 않으면, 예외 처리를 하기때문에 여기선 적절한 사용법으로 보임
    //findByIdAndUserId(Long id, Long userId): Board 의 id 와 userId 가 일치하는 Board 를 가져온다
        //--> 그래야, 로그인한 해당 사용자가 작성한 게시글만 수정, 삭제되기 때문(본인 인증)
    Optional<Board> findByIdAndUserId(Long id, Long userId);
}

UserRepository

package com.sparta.springboard.repository;

import com.sparta.springboard.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

//<연결할 엔티티(테이블) 클래스 이름, ID 필드 타입> --> User 에서 id 필드의 타입이 Long 이기 때문
public interface UserRepository extends JpaRepository<User, Long> {

// 회원 중복 확인
    //Optional: null 을 반환하면 오류가 발생할 가능성이 매우 높은 경우에 '결과 없음'을 명확하게 드러내기 위해
        //--> 작성, 수정, 삭제에서 해당 Username 이 DB 에 있는지 확인하고, 없으면 예외 처리를 하기때문에 여기선 적절한 사용법으로 보임
    Optional<User> findByUsername(String username);
}

service

Entity를 DTO 로 변환해서 반환(return)
--> DTO 로 변환하는 곳은 service 외에도 선택 가능하나, 각자의 장단점이 존재함.

BoardService

package com.sparta.springboard.service;

import com.sparta.springboard.dto.BoardRequestDto;
import com.sparta.springboard.dto.BoardResponseDto;
import com.sparta.springboard.dto.MsgResponseDto;
import com.sparta.springboard.entity.Board;
import com.sparta.springboard.entity.User;
import com.sparta.springboard.jwt.JwtUtil;
import com.sparta.springboard.repository.BoardRepository;
import com.sparta.springboard.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

//final 또는 @NotNull 이 붙은 필드의 생성자를 자동 생성. 주로 의존성 주입(Dependency Injection) 편의성을 위해서 사용
@RequiredArgsConstructor

public class BoardService {

    //의존성 주입
        //외부(다른 패키지)에서 ...?
    private final BoardRepository boardRepository;
    private final UserRepository userRepository;
    private final JwtUtil jwtUtil;

//게시글 작성
    //클래스나 메서드에 붙여줄 경우, 해당 범위 내 메서드가 트랜잭션(데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위(더 이상 쪼개질 수 없는 최소의 연산))이 되도록 보장
    //사용 이유?
        //1. 해당 메서드를 실행하는 도중 메서드 값을 수정/삭제하려는 시도가 들어와도, 값의 신뢰성이 보장
        //2. 연산 도중 오류가 발생해도, rollback 해서 DB에 해당 결과가 반영되지 않도록 함
    //예시: 결제는 다른 사람과 독립적으로 이루어지며, 과정 중에 다른 연산이 끼어들 수 없다. 오류가 생긴 경우 연산을 취소하고 원래대로 되돌린다. 성공할 경우 결과를 반영한다
    @Transactional   //업데이트를 할 때, DB에 반영이 되는 것을 스프링에게 알려줌??
    //BoardResponseDto 반환 타입, createBoard 메소드 명
    //BoardRequestDto: 넘어오는 데이터를 받아주는 객체, HttpServletRequest request 객체: 누가 로그인 했는지 알기위한 토큰을 담고 있음
    public BoardResponseDto createBoard(BoardRequestDto requestDto, HttpServletRequest request) {
        //request 에서 Token 가져오기
        String token = jwtUtil.resolveToken(request);
        //JWT 안에 있는 정보를 담는 Claims 객체
        Claims claims;

        //토큰이 있는 경우에만 추가 가능
        if (token != null) {
            //validateToken()를 사용해서, 들어온 토큰이 위조/변조, 만료가 되지 않았는지 검증
            if (jwtUtil.validateToken(token)) {
                //true 라면, 토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            //false 라면,
            } else {
                //매개변수가 의도치 않는 상황 유발시, 해당 메시지 반환
                throw new IllegalArgumentException("Token Error");
            }

            //토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            //claims.getSubject(): 우리가 넣어두었던 username 가져오기
            //findByUsername()를 사용해서, UserRepository 에서 user 정보를 가져오기
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    //매개변수가 의도치 않는 상황 유발시
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            //요청받은 DTO 로 DB에 저장할 객체 Board board 만들기
            Board board = boardRepository.saveAndFlush(new Board(requestDto, user.getId()));     //saveAndFlush 맞나?
			//entity(board) 를 DTO(BoardResponseDto) 로 변환시켜서 리턴(반환)
            return new BoardResponseDto(board);
        //토큰이 null 이라면(Client 에게서 Token 이 넘어오지 않은 경우),
        } else {
            //null 을 반환
            return null;        //trouble shooting: 중괄호 문제로 else return null; 에 빨간 글씨 --> 복붙할때 주의하자
        }
    }


//전체 게시글 목록 조회
    //(readOnly = true): JPA 를 사용할 경우, 변경감지 작업을 수행하지 않아 성능상의 이점
    @Transactional(readOnly = true)
    //BoardResponseDto 를 List 로 반환하는 타입, getListBoards 메소드 명, () 전부 Client 에게로 반환하므로 비워둠
    public List<BoardResponseDto> getListBoards() {
        //boardRepository 와 연결해서, 모든 데이터들을 내림차순으로, List 타입으로 객체 Board 에 저장된 데이터들을 boardList 안에 담는다
        List<Board> boardList =  boardRepository.findAllByOrderByModifiedAtDesc();      //주의. boards 와 board
        //boardResponseDto 를 새롭게 만든다 --> 텅 빈 상태 (빈 주머니 상태?)
        List<BoardResponseDto> boardResponseDto = new ArrayList<>();

        //반복문을 이용하여, boardList 에 담긴 데이터들을 객체 Board 로 모두 옮긴다
        for (Board board : boardList) {
            //board 를 새롭게 BoardResponseDto 로 옮겨담고, BoardResponseDto 를 boardResponseDto 안에 추가(add)한다
            boardResponseDto.add(new BoardResponseDto(board));
        }
        //최종적으로 옮겨담아진 boardResponseDto 를 반환
        return boardResponseDto;
    }

//선택한 게시글 조회
    @Transactional(readOnly = true)
    //BoardResponseDto 반환 타입, getBoard 메소드 명, Long id: 담을 데이터
    public BoardResponseDto getBoard(Long id) {
        //Board: Entity 명, boardRepository 와 연결해서, id 를 찾는다
        Board board = boardRepository.findById(id).orElseThrow(
                //매개변수가 의도치 않는 상황 유발시
                () -> new IllegalArgumentException("아이디가 존재하지 않습니다.")
        );
        //데이터가 들어간 객체 board 를 BoardResponseDto 로 반환
        return new BoardResponseDto(board);
    }

//선택한 게시글 수정(변경)
    @Transactional
    //BoardResponseDto 반환 타입, updateBoard 메소드 명
    //Long id: 담을 데이터, BoardRequestDto: 넘어오는 데이터를 받아주는 객체, HttpServletRequest request 객체: 누가 로그인 했는지 알기위한 토큰을 담고 있음
    public BoardResponseDto updateBoard(Long id, BoardRequestDto requestDto, HttpServletRequest request) {

        //request 에서 Token 가져오기
        String token = jwtUtil.resolveToken(request);
        //JWT 안에 있는 정보를 담는 Claims 객체
        Claims claims;

        //토큰이 있는 경우에만 추가 가능
        if (token != null) {
            //validateToken()를 사용해서, 들어온 토큰이 위조/변조, 만료가 되지 않았는지 검증
            if (jwtUtil.validateToken(token)) {
                //true 라면, 토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            //false 라면,
            } else {
                //매개변수가 의도치 않는 상황 유발시, 해당 메시지 반환
                throw new IllegalArgumentException("Token Error");
            }

            // 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            //claims.getSubject(): 우리가 넣어두었던 username 가져오기
            //findByUsername()를 사용해서, UserRepository 에서 user 정보를 가져오기
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    //매개변수가 의도치 않는 상황 유발시
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            //내가 가지고 온 boardid 이면서, 그 board 가 동일한 userid 를 가지고 있는지까지 확인
                //즉, 현재 로그인한 user 가 선택한 board 가 맞는지 확인
            Board board = boardRepository.findByIdAndUserId(id, user.getId()).orElseThrow(       //추가: 실수    //id?? id, user.getId()??
                    //매개변수가 의도치 않는 상황 유발시
                    () -> new IllegalArgumentException("게시글이 존재하지 않습니다.")
            );

            //동일하다면, update() 메서드 사용
            board.update(requestDto);
			//entity(board) 를 DTO(BoardResponseDto) 로 변환시켜서 리턴(반환)
            return new BoardResponseDto(board);     //추가: 실수

        } else {
            return null;
        }
    }

//선택한 게시글 삭제
    @Transactional
    //MsgResponseDto 반환 타입, deleteBoard 메소드 명
    //Long id: 담을 데이터, HttpServletRequest request 객체: 누가 로그인 했는지 알기위한 토큰을 담고 있음
    public MsgResponseDto deleteBoard (Long id, HttpServletRequest request) {

        //request 에서 Token 가져오기
        String token = jwtUtil.resolveToken(request);
        //JWT 안에 있는 정보를 담는 Claims 객체
        Claims claims;

        //토큰이 있는 경우에만 추가 가능
        if (token != null) {
            //validateToken()를 사용해서, 들어온 토큰이 위조/변조, 만료가 되지 않았는지 검증
            if (jwtUtil.validateToken(token)) {
                //true 라면, 토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            //false 라면,
            } else {
                //매개변수가 의도치 않는 상황 유발시, 해당 메시지 반환
                throw new IllegalArgumentException("Token Error");
            }

            // 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            //claims.getSubject(): 우리가 넣어두었던 username 가져오기
            //findByUsername()를 사용해서, UserRepository 에서 user 정보를 가져오기
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    //매개변수가 의도치 않는 상황 유발시
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            //내가 가지고 온 boardid 이면서, 그 board 가 동일한 userid 를 가지고 있는지까지 확인
                //즉, 현재 로그인한 user 가 선택한 board 가 맞는지 확인
            Board board = boardRepository.findByIdAndUserId(id, user.getId()).orElseThrow(       //추가: 실수 findById(id) --> findById(id, user.getId()) --> findByIdAndUserId(id, user.getId())
                    //매개변수가 의도치 않는 상황 유발시
                    () -> new IllegalArgumentException("게시글이 존재하지 않습니다.")
            );

            //동일하다면, delete() 메서드 사용
            boardRepository.delete(board);

            return new MsgResponseDto("게시글 삭제 성공", HttpStatus.OK.value());
            //이렇게 new 연산자로 생성해주면, 위에서는 따로 DelResponseDto result = new DelResponseDto(); 이런거 안 만들어줘도 됨
            //return new BoardResponseDto("게시글 삭제 성공", HttpStatus.OK.value());

        } else {
            return new MsgResponseDto("게시글 작성자만 삭제 가능", HttpStatus.OK.value());
            //return new BoardResponseDto("게시글 작성자만 삭제 가능", HttpStatus.OK.value());
        }
    }
}

UserService

package com.sparta.springboard.service;

import com.sparta.springboard.dto.LoginRequestDto;
import com.sparta.springboard.dto.SignupRequestDto;
import com.sparta.springboard.entity.User;
import com.sparta.springboard.entity.UserRoleEnum;
import com.sparta.springboard.jwt.JwtUtil;
import com.sparta.springboard.repository.UserRepository;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import java.util.Optional;

@Service

//다른 계층에서 파라미터를 검증하기 위해서
@Validated          //추가

//final 또는 @NotNull 이 붙은 필드의 생성자를 자동 생성. 주로 의존성 주입(Dependency Injection) 편의성을 위해서 사용
@RequiredArgsConstructor

public class UserService {

//회원가입 구현

    //의존성 주입(DI)
    private final UserRepository userRepository;
    private final JwtUtil jwtUtil;  //의존성 주입(DI) --> jwtUtil.class 에서 @Component 로 빈을 등록했기때문에 가능
    private static final String ADMIN_TOKEN = "AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC";

    @Transactional
    //SignupRequestDto 에서 가져온 username, password 를 확인
    public void signup(@Valid SignupRequestDto signupRequestDto) {      //@Valid 추가 주의!
        String username = signupRequestDto.getUsername();
        String password = signupRequestDto.getPassword();

    // 회원 중복 확인
        //userRepository 에서 username 으로 실제 유저가 있는지 없는지 확인
        Optional<User> found = userRepository.findByUsername(username);  //userRepository 에 구현하기
        //중복된 유저가 존재한다면,
        if (found.isPresent()) {
            //해당 메시지 보내기
            throw new IllegalArgumentException("중복된 사용자가 존재합니다.");
        }

    // 사용자 ROLE(권한) 확인
        UserRoleEnum role = UserRoleEnum.USER;
        //SignupRequestDto 에서 admin 이 true 라면(admin 으로 로그인을 시도하려고 하는구나),
        if (signupRequestDto.isAdmin()) {
            //들어온 Token 값과 위의 검증을 위한 Token 값(위쪽에 AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC)이 일치하는지 확인
            if (!signupRequestDto.getAdminToken().equals(ADMIN_TOKEN)) {
                //일치하지 않으면(매개변수가 의도치 않는 상황 유발시), 해당 메시지를 보낸다
                throw new IllegalArgumentException("관리자 암호가 틀려 등록이 불가능합니다.");
            }
            //일치하면, user 를 admin 타입으로 바꾼다
            role = UserRoleEnum.ADMIN;
        }

        //가져온 username, password, role(UserRoleEnum)를 넣어서 저장(save)
        User user = new User(username, password, role);
        userRepository.save(user);
    }

//로그인 구현
    @Transactional(readOnly = true)
    //로그인이 되면, LoginRequestDto 로 username, password 가 넘어온다
    public void login(LoginRequestDto loginRequestDto, HttpServletResponse response) {
        String username = loginRequestDto.getUsername();
        String password = loginRequestDto.getPassword();

    // 사용자 확인
        //username 을 통해 확인해서, 있다면 User 객체에 담긴다
        User user = userRepository.findByUsername(username).orElseThrow(
                //없다면(매개변수가 의도치 않는 상황 유발시), 해당 메시지 보내기
                () -> new IllegalArgumentException("등록된 사용자가 없습니다.")
        );
    // 비밀번호 확인
        //User 객체에 들어있던 Password 와 가지고 온 Password(String password = loginRequestDto.getPassword() 에 있는) 가 일치하는지 확인
        //일치하지 않는다면, 해당 메시지 보내기
        if(!user.getPassword().equals(password)){
            throw  new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
        }

        //response 에 addHeader() 를 사용해서, Header 쪽에 값을 넣는데
            //AUTHORIZATION_HEADER: KEY 값
            //createToken(user.getUsername(), user.getRole()): Username(이름), Role(권한)을 넣어서 토큰을 만든다
        //위의 User 객체에서 유저 정보를 가져왔기 떄문에 사용 가능한 것
        //jwtUtil 를 사용하기 위해, 위에서 의존성 주입을 해줘야 함
        response.addHeader(JwtUtil.AUTHORIZATION_HEADER, jwtUtil.createToken(user.getUsername(), user.getRole()));
    }
}

SpringboardApplication

package com.sparta.springboard;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing      //JPA Auditing 을 활성화
@SpringBootApplication
public class SpringboardApplication {

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

application.properties

spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:db;MODE=MYSQL;
spring.datasource.username=sa
spring.datasource.password=

spring.thymeleaf.cache=false

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true

jwt.secret.key=7ZWt7ZW0OTntmZTsnbTtjIXtlZzqta3snYTrhIjrqLjshLjqs4TroZzrgpjslYTqsIDsnpDtm4zrpa3tlZzqsJzrsJzsnpDrpbzrp4zrk6TslrTqsIDsnpA=

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.0.0'
    id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com.sparta'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.jetbrains:annotations:20.1.0'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    implementation group: 'org.json', name: 'json', version: '20220924'

    compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'

    implementation 'org.springframework.boot:spring-boot-starter-validation'    //의존성을 위해 추가
}

tasks.named('test') {
    useJUnitPlatform()
}

profile
개발자로 거듭나기!

0개의 댓글