2024. 10. 16. 17:45ㆍSpring/JPA 개인과제
※ 일정관리 게시판 만들기(JPA기반)
ㄴ 레벨 필수 0,1,2,3,4,5
ㄴ 레벨 도전 1,2,3,4로 구성
※ 지난번에 Spring인데 view에서 통신하는 부분만 주로 다루어서 실수를 했다고 생각한다.
ㄴ 이번엔 좀 더 자세하게 Spring부분을 다룰 생각이다.
[ 필수 Lv.0 api설계서, ERD, 테이블 생성 쿼리문 제작 ]
ㄴ ERD
ㄴ API명세서
ㄴ 테이블 CREATE문
[ 필수 CRUD ]
※ CRUD가 레벨 4까지 테이블만 달라지고 겹치는 부분이 많아서 일정 기준으로 전체 CRUD를 설명하고 다른 기능들은 추가적으로 설명하는 방식으로 진행하겠음.
요구사항)
1. 해당 어노테이션 사용
2. CRUD 기능 구현
3. 일정 삭제시 댓글도 함께 삭제되도록(영속성 전이 기능 활용)
- 3번은 추후 댓글과 연동됨.
[ Entity관련 핵심 어노테이션 설명 ]
어노테이션 | 설명 |
@Entity | JPA에서 관리하는 클래스이며, 데이터베이스 테이블과 매핑할 클래스라는 뜻이다. - 기본생성자 필수 - 컬럼으로 지정할 필드에 final 사용 불가 - final,enum,interface, inner클래스에 사용불가 |
@Table | Entity와 매핑시킬 데이터베이스 테이블명을 지정해준다. - name : 테이블이름 |
@Column | 데이터베이스 컬럼과 필드를 매핑할정보를 넣는다. - name : 컬럼이름 - nullable=false : 컬럼 not null 체크 - length : 컬럼의 길이 지정 - 필드에서 설정한 자료형에따라 값이 바뀜 ex) @Column(length=20) private String text - varchar(20) |
[ Entity - Schedules.java ]
@Entity
@Getter
@Setter
@Table(name="schedules")
@NoArgsConstructor
public class Schedule extends Timestamped {
// 댓글과 연동 //
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Comment(value = "일정 고유번호")
private Long id;
@OneToMany(mappedBy = "s_id", cascade = CascadeType.REMOVE) // 일정 삭제시 댓글도 삭제되도록(영속성전이)
private List<Comments> comments = new ArrayList<>();
// 댓글과 연동 //
// 유저와 연동 - 필수4와 도전레벨과 연동 //
@JoinColumn(name="m_id" , referencedColumnName = "id")
@Comment(value = "유저 고유번호 - 유저 테이블과 연동")
private Long m_id;
@ManyToOne // 유저테이블과 연관관계 설정
private Member member;
// 유저와 연동 - 필수4와 도전레벨과 연동 //
@Column(name="m_name", length=45)
@Comment(value = "작성 유저명")
private String m_name;
@Column(name="title", nullable = false, length=255)
@Comment(value = "할일 제목")
private String title;
@Column(name="content", nullable = false, length=255)
@Comment(value = "할일 내용")
private String content;
@Column(name="weather", length=50)
@Comment(value = "날씨")
private String weather;
public Schedule(ScheduleRequestDto requestDto){
this.title = requestDto.getTitle();
this.content = requestDto.getContent();
this.m_id = requestDto.getM_id();
this.weather = requestDto.getWeather();
}
public void update(ScheduleRequestDto requestDto) {
this.title = requestDto.getTitle();
this.content = requestDto.getContent();
}
}
[코드설명]
1) @Entity 어노테이션으로 일정CRUD를 위한 데이터베이스 테이블과 매핑을 시킨다.
2) @Column 어노테이션으로 각 필드를 컬럼과 매핑시킨다.
- 요구사항에 있는 작성유저명,할일제목,할일내용 3가지 컬럼을 매핑했다.
3) @Id는 컬럼과 매핑하는 필드에 기본키를 설정하는 어노테이션이다. id(일정고유번호)에 기본키를 설정했다.
- @GeneratedValues는 컬럼에 auto_increment 설정을 해준다 - auto_increment는 자동생성 설정이다
- strategy = GenerationType.IDENTITY 해당 설정은 MySQL에서 사용한다. Oracle은 SEQUENCE을 사용한다고 한다.
4) 요구사항 3번 일정 삭제시 댓글삭제(영속성전이기능) 기능을 위해 @OneToMany 어노테이션으로 댓글테이블과 연관관계를 설정 후 cascade=CascadeType.REMOVE (영속성전이기능)으로 동시 삭제되도록 했다.
5) @Comment 어노테이션은 사용하는 것이 필수는 아니며, 각 컬럼에 comment를 넣어주는 역할이다.
ㄴ 아래와 같이 Comment가 들어가게 된다. 나중에 데이터베이스를 확인할때 보기 쉽게하기위해 본인은 습관이 되어있어서 적용했다.
6) @Getter, @Setter, @NoArgsConstructor 3가지 어노테이션은 lombok 라이브러리에서 제공하는 기능으로 선언한 필드들의 getter, setter메소드와 클래스의 기본생성자를 자동으로 생성해주는 어노테이션이다.
- 사용한 class파일에선 보이지 않고 자동으로 사용할 수 있게 해준다.
ㄴ 해당 Schedule entity의 빌드파일에서 볼 수 있듯이 해당 메소드와 생성자들이 선언되어 있는것을 볼 수 있다.
7) 가장 아래 schedule 클래스는 requestDto로 받은 정보를 일정등록할때 값을 전달하기위한 용도이고, update는 일정 수정시 활용하려는 메소드이다. 등록, 수정할때 항목이 다른것을 확인할 수 있다.
[ 영속성 전이-일정삭제시 댓글삭제 영상 ]
[ 연관관계 관련 핵심 어노테이션 설명 ]
어노테이션 | 설명 |
@OneToMany | 1:N관계에서 1의 해당하는 테이블에서 사용한다. - mappedBy로 주인관계 테이블의 외래키를 넣어준다.(본인은 주인이 아니다라는 뜻도 내포) - cascade로 영속성 전이기능을 사용할 수 있다. - ex) 일정테이블 예시 @OneToMany(mappedBy = "s_id", cascade = CascadeType.REMOVE) // 영속성 전이로 같이 삭제 private List<Comments> comments = new ArrayList<>(); // 댓글테이블과 연관관계 |
@ManyToOne | 1:N관계에서 N의 해당하는 테이블에서 사용한다. - 외래키를 가지고 있는 주인관계의 테이블에 사용을한다. - @JoinColumn 어노테이션을 사용하여 외래키로 지정할 컬럼과 연관테이블의 컬럼을 지정한다. - ex) 댓글테이블 예시 @JoinColumn(name="s_id" , referencedColumnName = "id") @Comment(value = "일정 고유번호 - 일정 테이블과 연동") private Long s_id; // 댓글테이블의 s_id를 일정테이블의 id(기본키)와 연동하여 외래키로 설정 @ManyToOne private Schedule schedule // 일정 테이블과 연관관계 |
※ 이후 레벨의 entity들도 다 동일한 형태로 진행되며, 위 설명 끝까지 참고바람.
[ Dto - ScheduleRequestDto.java, ScheduleResponseDto.java ]
-------- Request --------
@Getter
@Setter
public class ScheduleRequestDto {
private Long id;
private Long m_id;
private String m_name;
private String weather;
@NotBlank(message = "할일 제목은 필수로 입력해주시길 바랍니다.")
@Size(max=255, message = "255자 이하로 입력해주시길 바랍니다.")
private String title;
@NotBlank(message = "할일 내용은 필수로 입력해주시길 바랍니다.")
@Size(max=255, message = "255자 이하로 입력해주시길 바랍니다.")
private String content;
}
-------- Response --------
@Getter
public class ScheduleResponseDto {
private Long id;
private Long m_id;
private String m_name;
private String title;
private String content;
private String weather;
private LocalDateTime reg_date;
private LocalDateTime edit_date;
public ScheduleResponseDto(Schedule schedule) {
this.id = schedule.getId();
this.m_id = schedule.getM_id();
this.m_name = schedule.getM_name();
this.title = schedule.getTitle();
this.content = schedule.getContent();
this.weather = schedule.getWeather();
this.reg_date = schedule.getReg_date();
this.edit_date = schedule.getEdit_date();
}
}
[코드설명]
1) 일정 등록, 수정 시 활용되는 RequestDto파일이다 사용자가 입력하는 값을 처리하기위한 Dto파일이다.
- RequestDto에서 눈에 띄는 것은 2가지 항목만 특정 예외처리가 들어가 있는 것을 볼 수 있다.
- 할일제목, 할일내용만 등록,수정시 들어가는 항목이며, 예외처리는 레벨5에서 자세히 다루도록 하겠다.
2) ResponseDto는 CRUD같은 기능이 실행되고 난 후 반환값을 확인하기 위한 Dto파일이다.
- 일정조회할때 ResponseDto를 반환해서 조회한다.
[ Controller- ScheduleController.java ]
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class ScheduleController {
private final ScheduleService scheduleService;
private final JwtUtil jwtUtil;
// 일정 등록
@PostMapping("/schedule/{id}")
public ScheduleResponseDto createSchedule(@PathVariable Long id, @RequestBody @Valid ScheduleRequestDto requestDto) {
return scheduleService.createSchedule(id,requestDto);
}
// 일정 전체조회
@GetMapping("/schedule")
public List<ScheduleResponseDto> getSchedule() {
return scheduleService.getSchedule();
}
// 일정 페이징 조회
@GetMapping("/schedule/page")
public List<SchedulePagingDto> getSchedulePaging(@PageableDefault(size = 10, sort = "edit_date", direction = Sort.Direction.DESC) Pageable pageable) {
return scheduleService.getSchedulePaging(pageable);
}
// 일정 수정
@PutMapping("/schedule/{id}")
public Long updateSchedule(@CookieValue(JwtUtil.AUTHORIZATION_HEADER) String tokenValue, @PathVariable Long id, @RequestBody @Valid ScheduleRequestDto requestDto) {
return scheduleService.updateSchedule(tokenValue,id,requestDto);
}
// 일정 삭제
@DeleteMapping("/schedule/{id}")
public Long deleteSchedule(@CookieValue(JwtUtil.AUTHORIZATION_HEADER) String tokenValue, @PathVariable Long id) {
return scheduleService.deleteSchedule(tokenValue,id);
}
}
[코드설명]
1) CRUD 및 일정과 관련된 기능에 대한 요청을 하는 파일이다.
- 3계층구조에 따라 controller, service, repository로 이어지고, 가장 첫번째 단계이다.
2) 등록, 수정을하는 createSchedule, updateSchedule은 @RequestBody 어노테이션을 사용해서 입력값을 requestDto로 전달하는 것을 확인할 수 있다.
3) 수정, 삭제를 할때는 일정id를 선택하고 해당 id만 수정 및 삭제가 되어야 하기 때문에 @PathVariable 어노테이션을 사용해서 값을 전달하고 있다.
4) 일정조회는 responseDto에서 값을 확인하는 형태로 되어있다.
- 위 responseDto에서 볼 수 있듯이 각 필드들의 get메소드를 통해서 값을 조회한다.
※ 중간에 페이징이나, jwt, @CookieValue등의 기능은 높은레벨에서 진행되는 기능으로 추후에 설명하도록 하겠다.
[ Service - ScheduleService.java ]
@Service
public class ScheduleService {
private final ScheduleRepository scheduleRepository;
private final MemberRepository memberRepository;
public ScheduleService(ScheduleRepository scheduleRepository, MemberRepository memberRepository) {
this.scheduleRepository = scheduleRepository;
this.memberRepository = memberRepository;
}
// 일정생성 메소드
public ScheduleResponseDto createSchedule(ScheduleRequestDto requestDto) {
Schedule schedule = new Schedule(requestDto);
Schedule saveSchedule = scheduleRepository.save(schedule);
ScheduleResponseDto scheduleResponseDto = new ScheduleResponseDto(saveSchedule);
return scheduleResponseDto;
}
// 일정 전체 조회
public List<ScheduleResponseDto> getSchedule() {
return scheduleRepository.findAllBy().stream().map(ScheduleResponseDto::new).toList();
}
// 일정 수정
@Transactional
public Long updateSchedule(Long id, ScheduleRequestDto requestDto) {
Schedule schedule = findSchedule(id);
schedule.update(requestDto);
return id;
}
public Long deleteSchedule(Long id) {
Schedule schedule = findSchedule(id);
scheduleRepository.delete(schedule);
return id;
}
// 일정 ID 조회
private Schedule findSchedule(Long id) {
return scheduleRepository.findById(id).orElseThrow(() ->
new IllegalArgumentException("선택한 일정은 존재하지 않습니다.")
);
}
}
[코드설명]
※ 설명하기에 앞서 현재 코드는 높은 레벨의 기능들로 인해서 코드가 많이 길어져서 레벨1에 해당되는 기능들만 추려서 가져왔습니다. controller와 매개변수나 특정부분들이 다를 수 있는데 양해 바라겠습니다.
1) Controller에서 전달받아 비즈니스 로직을 처리하는 계층이다.
2) @Service 어노테이션은 해당클래스를 Spring 프레임워크의 Bean으로 등록하는 역할을 한다.
3) 수정, 삭제를 할때는 일정id를 조회해서 선택한 일정이 존재하는지 여부를 확인 후 예외처리를 한다.
4) 등록,삭제는 JPA에서 제공하는 save,delete메소드를 활용하여 수행한다.
5) 수정은 Entity에서 만든 update메소드를 활용하여 수행한다.
[ Repository - ScheduleRepository.java ]
@Repository
public interface ScheduleRepository extends JpaRepository<Schedule, Long> {
List<Schedule> findAllBy(); // 일정 전체 조회
}
[코드설명]
1) 3계층에서 마지막 단계로 DB에 관한 작업들을 진행하는 Repository이다.
- Entity로 생성된 DB와 DB접근하는 메서드를 사용하기 위한 인터페이스이다.
- JpaRepository을 상속받으면 Jpa에서 기본적으로 제공하는 메서드들을 사용할 수 있다.
2) @Repository 어노테이션으로 해당클래스를 Spring 프레임워크의 Bean으로 등록하는 역할을 한다.
3) Service에서 보면 알 수 있듯이 DB관련된 메서드들을 ScheduleRepository에서 전달받아 사용하고있다.
※ 각 계층과 Dto 또는 어노테이션 관련해서 미흡한 부분은 계속 채워나가도록 하겠습니다. Entity부터 Repository까지 총 3개 테이블 유저,일정,댓글에 대한 모든 프로세스가 동일하게 진행이 됩니다. 아래부터는 특정 기능들에 대해서 추가 설명을 하겠습니다.
[작성일 수정일]
ㄴ JPA Auditing은 여러 Entity들에 대해서 공통으로 들어가는 컬럼들을 하나의 Entity로 설정할때 사용하는 기능이다.
ㄴ JPA Auditing 기능을 활용하여 작성일, 수정일을 Entity로 따로 만든다.
// Application 클래스에 @EnableJpaAuditing 활용하여 기능 활성화
@EnableJpaAuditing
@SpringBootApplication
public class JpaScheduleApplication {
public static void main(String[] args) {
SpringApplication.run(JpaScheduleApplication.class, args);
}
}
------ Timestamped 클래스 ------
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
// 작성일, 수정일 전체 ENTITY 에 적용
public abstract class Timestamped {
@CreatedDate
@Column(name="reg_date", updatable = false)
@Comment(value = "작성일 - 작성되는 동시 자동")
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime reg_date;
@LastModifiedDate
@Column(name="edit_date")
@Comment(value = "수정일 - 수정할때 UPDATE")
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime edit_date;
}
1) @MappedSuperclass
- 상속 받는 자식클래스에게 정보만 전달하고 다른 매핑은 하지않도록 하는 어노테이션
2) @EntityListeners(AuditingEntityListener.class)
- Auditing기능을 사용하겠다고 JPA에게 알려주는 용도
3) adstract로 클래스를 선언하여 다른 Entity클래스에서 상속 받을 수 있도록 해준다.
4) 아래에는 공통으로 적용할 컬럼을 위에 Entity에서 보듯이 설정하여 필드와 컬럼을 매핑해준다.
ㄴ 일정,유저,댓글 Entity에서 상속해서 사용하고 있는 것을 볼 수 있다.
[일정 페이징]
요구사항)
1. 해당 어노테이션 사용
2. Pageable 활용하여 페이징 기능 구현
3. 디폴트 게시글 10개, 요구사항에 있는 6가지 항목 조회 / 일정 수정일 내림차순
[ 페이징 전체파일(총5개) - controller,service,repository,Dto,paging인터페이스 ]
// [Controller]
@GetMapping("/schedule/page")
public List<SchedulePagingDto> getSchedulePaging(@PageableDefault(size = 10, sort = "edit_date", direction = Sort.Direction.DESC) Pageable pageable) {
return scheduleService.getSchedulePaging(pageable);
}
// [Service]
public List<SchedulePagingDto> getSchedulePaging(Pageable pageable) {
return scheduleRepository.findAllBy(pageable).stream().map(SchedulePagingDto::new).toList();
}
// [PagingDto]
@Getter
public class SchedulePagingDto {
private String title;
private String content;
private LocalDateTime reg_date;
private LocalDateTime edit_date;
private String memberName; // 작성자 유저명 (서브쿼리)
private int count; // 댓글 개수 (서브쿼리)
public SchedulePagingDto(PagingColumn pagingColumn) {
this.title = pagingColumn.getTitle();
this.content = pagingColumn.getContent();
this.reg_date = pagingColumn.getReg_date();
this.edit_date = pagingColumn.getEdit_date();
this.memberName = pagingColumn.getMemberName();
this.count = pagingColumn.getCount();
}
}
// [페이징 인터페이스]
public interface PagingColumn {
String getTitle();
String getContent();
LocalDateTime getReg_date();
LocalDateTime getEdit_date();
String getMemberName(); // 작성자 유저명 (서브쿼리)
int getCount(); // 댓글 개수 (서브쿼리)
}
// [Repository]
@Repository
public interface ScheduleRepository extends JpaRepository<Schedule, Long> {
// 페이징 쿼리
@Query(value=
"select" +
" title," +
" content," +
" reg_date," +
" edit_date," +
" (SELECT name FROM members WHERE id = schedules.m_id) as memberName, "+
" (SELECT COUNT(id) FROM comments WHERE s_id = schedules.id) as count "+
" from schedules",
nativeQuery = true
)
List<PagingColumn> findAllBy(Pageable pageable);
}
[코드설명]
1) Controller에서 Pageable 객체를 사용하고있다.
2) @PageableDefault 어노테이션으로 기본 게시글수 10개, 수정일로 내림차순 정렬까지 설정한다.
- sort=" 정렬할 컬럼" direction= Sort.Direction.DESC(내림차순)
3) Service에서 Pageable 객체만 매개변수 값으로 넣는다.
4) Controller와 Service 둘다 SchedulePagingDto로 반환해준다
- 이유는 페이징시에 조회하는 컬럼이 6개인데 테스트 후 반환 하는 값을 페이징에 맞는값으로 맞추기위함이다.
5) PagingColumn 인터페이스는 Repository에서 요구사항 항목에 맞게 반환을 위한 값을 담아둔다.
- 할일제목, 할일내용, 작성일, 수정일, 작성자유저명, 댓글 개수
6) Repository에서 findAllBy에 Pageable객체를 담고, PagingColumn 인터페이스를 참조하여 위에서 세팅한 값들을 다사용해준다.
- 요구사항에서 작성자유저명, 댓글개수 2가지 항목은 서브쿼리로 불러와야하는 항목이기 때문에 쿼리문을 직접 작성.
- @Query 어노테이션을 활용하여 만들어둔 SELECT문을 넣고, nativeQuery=true로 설정을 한다.
7) API테스트 시 기본은 10개 데이터를 불러오고, page와 size를 param을 통해서 넣어주면 값에 맞게 개수를 가져온다.
[ 페이징 영상 ]
[예외처리]
요구사항)
1. 어노테이션 참고 캡쳐본을 보고 필요한 예외처리를 해보기.
2. 이메일 @Pattern 어노테이션을 사용하여 예외처리 하기.
[ 예외처리 전체파일(총2개) - controller,Dto]
// [MemberController] - 유저Entity에 대한 기능 (등록,수정만 발췌)
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class MemberController {
private final MemberService memberService;
// 유저 등록(회원가입)
@PostMapping("/member")
public MemberResponseDto createMember(@RequestBody @Valid MemberRequestDto requestDto) {
return memberService.createMember(requestDto);
}
// 유저 수정
@PutMapping("/member/{id}")
public Long updateMember(@PathVariable Long id, @RequestBody @Valid MemberRequestDto requestDto) {
return memberService.updateMember(id,requestDto);
}
}
// [MemberRequestDto] - 예외처리
@Getter
@Setter
public class MemberRequestDto {
private Long id;
private int authority_code;
private String authority_name;
private String adminToken = "";
@NotBlank(message = "이름은 필수로 입력해주시길 바랍니다.")
@Size(max=45, message = "45자 이하로 입력해주시길 바랍니다.")
private String name;
@Size(max=255, message = "255자 이하로 입력해주시길 바랍니다.")
@Pattern(regexp = "^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$", message = "이메일 형식을 맞춰서 입력해주시길 바랍니다.")
private String email;
private String pw;
}
[코드설명]
1) Entity에서 컬럼 매핑을 끝내고 requestDto에서 예외처리를 하고 있다.
- @size : max, min 등 최대값 최소값을 설정할 수 있고, message를 넣어서 해당 예외시 메시지를 반환한다.
- @NotBlank : null, ""(빈공백) / null과 빈공백을 체크하며, 문자열 타입만 사용할 수 있습니다.
- @Pattern : 정규식과 일치하는지 검증되는데 사용됨.
2) 이름 @ NotBlank,@Size / 이메일 @Pattern, @Size를 사용하여 예외처리 3가지를 하고있다.
※ 다른 테이블의 컬럼도 예외처리 중이지만 다 동일한 어노테이션으로 member만 예시코드로 선택\
3) 상단에 Controller에서 @Vaild 어노테이션을 활용해서 예외처리를 진행하고 있다.
- @RequestBody 객체를 검증한다.
[ 예외처리 영상 ]
[총정리]
※ 각 레벨에 대한 기능과 코드를 설명하였고, 부족한 부분은 추후에 채워갈 예정입니다.
레벨 | 기능 내용 |
레벨 0 | API명세서, 테이블생성쿼리문, ERD |
레벨 1,2,4 | 각 테이블에 대한 CRUD |
레벨 3,5 | 일정페이징, 예외처리 |
기타 | 일정삭제시 댓글삭제 - (영속성전이), 각 Entity에 대한 연관관계 설정하기. |
'Spring > JPA 개인과제' 카테고리의 다른 글
[13] Spring - lv 5 내가 정의한 문제와 해결과정 (8) | 2024.10.31 |
---|---|
[12] Spring - 심화과제 aop 등 트러블슈팅 (8) | 2024.10.31 |
[8] Spring - 개인과제_2차 JPA 다루기 (도전레벨) (12) | 2024.10.17 |
[7] Spring - 개인과제_2차 JPA 다루기 트러블 슈팅 (11) | 2024.10.17 |