elevne's Study Note
Spring Boot Project (1) 본문
간단한 게시판 서버를 Spring Boot 를 통해 천천히 구현해보고자 하였다. 우선 아래와 같은 dependency 들만 추가해주었다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
간단하게 게시글, 댓글, 유저 엔티티를 만들어보기로 하였다. 엔티티를 본격적으로 작성하기에 앞서 TimeEntity 클래스를 아래와 같이 작성해보았다.
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityCallback.class)
public abstract class TimeEntity {
@Column(name = "CREATED_DATE")
@CreatedDate
private String createdDate;
@Column(name = "MODIFIED_DATE")
@LastModifiedDate
private String modifiedDate;
@PrePersist
public void prePersist(){
this.createdDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
this.modifiedDate = this.createdDate;
}
@PreUpdate
public void preUpdate(){
this.modifiedDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
}
@MappedSuperclass 어노테이션은 객체의 입장에서 공통 매핑 정보가 필요할 때 사용된다. 위와 같이 다양한 클래스가 생성시간, 수정시간과 같은 속성을 동일하게 가질 때, MappedSuperclass 어노테이션이 붙은 위 클래스를 각각의 클래스에서 상속받게끔 하는 것이다. 테이블과 관계 없이, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할을 하며, 이는 엔티티가 아니다. 이는 직접 생성해서 사용할 일이 없으니 추상 클래스로 사용하는 것이 권장된다고 한다.
@EntityListeners 는 JPA Entity 에 이벤트가 발생할 때 콜백을 처리하고 코드를 실행하는 방법이다.
위 @EntityListeners 를 사용하기 위해서는 우선 어플리케이션 구동 클래스에 @EnableJpaAuditing 어노테이션을 붙여준다. 이를 사용하면 @CreatedBy (작성자), @CreatedDate (작성일), @LastModifiedDate (수정일) 를 자동으로 넣는 기능을 사용할 수 있다.
그 다음으로는 이 TimeEntity 를 상속받는 엔티티를 작성해주었다. 먼저 User 엔티티이다.
@Entity
@Table(name = "USER")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class User extends TimeEntity {
@Id
@Column(name = "USER_UUID")
@GeneratedValue(strategy = GenerationType.UUID)
private String userUUID;
@Column(name = "USER_ID", nullable = false, updatable = false, length = 20)
private String userId;
@Column(name = "PASSWORD", length = 50)
private String password;
@Column(name = "USER_ROLE")
@Enumerated(EnumType.STRING)
private UserRole userRole;
@RequiredArgsConstructor
public enum UserRole {
USER("USER"), ADMIN("ADMIN");
private final String role;
}
}
@NoArgsConstructor 은 파라미터가 없는 기본 생성자를, @AllArgsConstructor 은 모든 필드 값을 파라미터로 받는 생성자를 만들어준다. @RequiredArgConstructor 은 final 이나 @NonNull 인 필드 값만 파라미터로 받는 생성자를 만들어준다.
@Enumerated 는 enum 클래스의 정보를 DB 에 저장할 때 사용된다. 내부에 EnumType.String 을 작성하면 enum 의 이름을 DB 에 저장하고, EnumType.ORDINAL 을 넣으면 순서 값을 DB 에 저장한다.
@Entity
@Table(name = "POST")
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Post extends TimeEntity {
@Id
@Column(name = "POST_ID")
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long postId;
@Column(name = "CATEGORY", nullable = true)
private String category;
@Column(name = "TITLE", nullable = false, length = 100)
private String title;
@Column(name = "CONTENT", nullable = false)
private String content;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "USER_ID")
private User user;
@OneToMany(mappedBy = "post", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
@OrderBy("CREATED_DATE DESC")
private List<Comment> comments;
}
그 다음으로는 Post 엔티티를 작성했다. User 에서도 사용되었는데, @Id 로 지정된 컬럼에 @GeneratedValue 어노테이션이 작성되었다. @Id 는 PK 를 나타내기 위해 사용되며, @GeneratedValue 는 생성전략을 정의해준다. @GeneratedValue 내에는 GenerationType, 기본키 생성 전략을 설정해주게 된다. 그 종류로는 TABLE, SEQUENCE, IDENTITY, AUTO 가 있다. TABLE 은 데이터베이스에 키 생성 전용 테이블을 하나 만들고 이를 이용하여 기본키를 생성한다고 한다. SEQUENCE 는 DB 에서 유일한 값을 순서대로 생성하는 특별한 오브젝트로,이를 이용하여 PK 를 생성한다. IDENTITY 는 기본키 생성을 데이터베이스에 위임하게 된다. (e.g., MySQL 은 AUTO_INCREMENT 를 사용하여 기본키 생성) AUTO 는 JPA 구현체가 자동으로 생성 전략을 결정한다.
그 다음으로 테이블간 관계 설정을 위해 @ManyToOne 과 @OneToMany 어노테이션이 사용됐다. 여러 개의 Post 가 하나의 User 에 속할 수 있으므로 @ManyToOne, 하나의 Post 에 대해 여러 개의 Comment 가 들어올 수 있으므로 @OneToMany 가 각각 사용된다. 위 User 과 Post 테이블은 단방향 관계로, Post 가 연관관계의 주인임을 나타내고 물리 테이블에 있는 USER_ID 컬럼을 통해 USER 필드를 채우기 위해 @JoinColumn 을 사용한다. @ManyToOne 을 양방향으로 사용할 수도 있다. 이 때는 관계의 주인이 아닌 쪽에서 주인의 정보를 조회할 수 있어야 하는 것이다. Post 와 Comment 의 관계에 있어서 Comment 가 주인이지만, Post 에서 이를 조회하기 위해서 위와 같이 @OneToMany(mappedBy="...") 를 적어주면 된다.
@ManyToOne 과 @OneToMany 에 FetchType 이라는 것이 들어간 것을 확인할 수 있다. FetchType.LAZY 를 사용하면 지연로딩이 사용된다. 해당 엔티티가 로딩되는 시점에 LAZY 로딩이 설정되어 있는 내부의 다른 엔티티는 프록시 객체로 가져오게 된다. 후에 실제 객체를 사용하는 시점에 이는 초기화되고 DB 에 쿼리가 실행된다. 반대로 FetchType.EAGER 을 사용하면, 조인을 사용하여 한 번에 전부 조회하게 되는 것이다.
또, CascadeType 이라는 것도 사용되었다. CascadeType.ALL 을 지정해주면 상위 엔티티에서 하위 엔티티로 모든 작업을 전파한다. PERSIST 는 하위 엔티티까지 영속성을 전달하여, 상위 엔티티를 저장하면 하위 엔티티도 자동 저장된다. MERGE 는 하위 엔티티까지 병합 작업을 지속한다. REMOVE 는 하위 엔티티까지 제거 작업을 지속한다. REFRESH 는 상위 엔티티를 다시 읽어올 때 하위엔티티도 데이터베이스로부터 인스턴스 값을 다시 읽어 온다.
@Entity
@Table(name = "COMMENT")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Comment extends TimeEntity {
@Id
@Column(name = "COMMENT_ID")
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long commentId;
@Column(name = "CONTENT")
private String content;
@ManyToOne
@JoinColumn(name = "POST_ID")
private Post post;
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
}
위와 같이 세 가지의 엔티티를 만들어준 이후 간단한 테스트를 진행해보았다.
@SpringBootTest
class BoardApplicationTests {
@Autowired
private PostRepository postRepository;
@Autowired
private UserRepository userRepository;
@Test
@DisplayName("유저생성 및 글 저장 테스트")
@Rollback(value = false)
@Transactional
void testJPA() {
User user1 = new User();
user1.setUserId("TEST_ID");
user1.setPassword("TEST_PWD");
user1.setUserRole(User.UserRole.USER);
userRepository.save(user1);
Post post = new Post();
post.setUser(user1);
post.setCategory("TEST_CATEGORY");
post.setTitle("TEST_TITLE");
post.setContent("TEST_CONTENT");
postRepository.save(post);
List<Post> result = postRepository.findAllByUser(user1);
assertEquals("TEST LIST SIZE", result.size(),1);
assertEquals("TEST POST", result.get(0).getTitle(), "TEST_TITLE");
}
}
위와 같이 @Rollback 을 사용해주면 테스트가 끝나 후에도 데이터가 삭제되지 않고 DB 에 남아있게 된다. 위 테스트는 잘 통과한다!
https://data-make.tistory.com/668
https://ict-nroo.tistory.com/132
https://soojong.tistory.com/entry/JPA-ManyToOne-OneToMany-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0
https://rutgo-letsgo.tistory.com/306
'Backend > Spring' 카테고리의 다른 글
Spring Security : JWT 적용해보기 (2) (0) | 2023.06.11 |
---|---|
Spring Security : JWT 적용해보기 (1) (0) | 2023.06.10 |
Spring Boot 복습 (6) (0) | 2023.04.10 |
Spring Boot 복습 (5) (0) | 2023.04.09 |
Spring Boot 복습 (4) (0) | 2023.04.08 |