UI 방식 선택(Thymeleaf vs React) + JPA/Validation 추가 + 샘플 데이터 생성
이번 글은 2강과 3강 내용을 한 번에 정리합니다.
UI를 어디서 렌더링하느냐에 따라 구조가 갈립니다.
Thymeleaf / JSP 방식
→ 스프링부트 “안”에 뷰(템플릿)가 포함됨
→ 서버가 HTML을 만들어서 내려줌(서버 사이드 렌더링)
React / Vue / Svelte 방식
→ UI가 스프링부트 “밖”에 존재함
→ 서버는 데이터(JSON)만 제공하고, 화면은 프론트 앱이 렌더링(클라이언트 사이드 렌더링)
@ControllerModel에 값을 담음예시(개념):
@Controller
public class PostViewController {
@GetMapping("/posts")
public String posts(Model model) {
model.addAttribute("posts", postService.findAll());
return "posts"; // posts.html
}
}
✅ 장점
@RestController예시(개념):
@RestController
@RequestMapping("/api/v1/posts")
public class ApiV1PostController {
@GetMapping
public List<Post> posts() {
return postService.findAll();
}
}
✅ 장점
UI가 스프링부트 내부(Thymeleaf)에 있으면 서버가 HTML을 렌더링하고,
UI가 외부(React)에 있으면 서버는 JSON만 제공한다.
3강에서는 본격적으로 “게시글 + 댓글” 도메인을 만들기 위해
JPA 엔티티 / 레포지토리 / 서비스를 구성하고,
실습 편의를 위해 샘플 데이터를 자동 생성하도록 설정합니다.
실습에서는 여러 엔티티가 공통으로 가져야 하는 필드(예: id, 생성/수정 시간)를
BaseEntity로 묶어두는 패턴을 사용합니다.
@MappedSuperclass: 상속받는 엔티티에 필드가 포함되도록@EntityListeners(AuditingEntityListener.class): Auditing 활성화@CreatedDate, @LastModifiedDate: 생성/수정 시간 자동 기록그리고
BackApplication에서@EnableJpaAuditing을 켜줘야 auditing이 동작합니다.
@OneToMany)mappedBy = "post"로 연관관계의 주인을 PostComment.post로 지정cascade = {PERSIST, REMOVE}, orphanRemoval = true로 댓글 생명주기 관리@Entity
@Getter
@NoArgsConstructor
public class Post extends BaseEntity {
private String title;
private String content;
@OneToMany(mappedBy = "post", fetch = LAZY, cascade = {PERSIST, REMOVE}, orphanRemoval = true)
private List<PostComment> comments = new ArrayList<>();
public Post(String title, String content) {
this.title = title;
this.content = content;
}
public PostComment addComment(String content) {
PostComment postComment = new PostComment(this, content);
comments.add(postComment);
return postComment;
}
}
@ManyToOne)@Entity
@Getter
@NoArgsConstructor
public class PostComment extends BaseEntity {
@ManyToOne
private Post post;
private String content;
public PostComment(Post post, String content) {
this.post = post;
this.content = content;
}
public void modify(String content) {
this.content = content;
}
}
⚠️ 참고
이후 REST API에서 엔티티를 그대로 JSON으로 반환하면
양방향 관계 때문에 Jackson 순환참조 문제가 발생할 수 있고(4강 포인트),
그때@JsonIgnore또는 DTO로 해결하게 됩니다.
JpaRepository<Post, Integer>로 기본 CRUD 사용
public interface PostRepository extends JpaRepository<Post, Integer> {
}
서비스는 레포지토리를 이용해 글 작성/조회 등을 담당합니다.
@Service
@RequiredArgsConstructor
public class PostService {
private final PostRepository postRepository;
public long count() {
return postRepository.count();
}
public Post write(String title, String content) {
Post post = new Post(title, content);
return postRepository.save(post);
}
public Optional<Post> findById(int id) {
return postRepository.findById(id);
}
public List<Post> findAll() {
return postRepository.findAll();
}
}
서버 실행 시 데이터가 없으면 자동으로 넣어주도록 BaseInitData를 구성합니다.
ApplicationRunner로 앱 실행 직후 실행@Transactional로 영속성 컨텍스트/변경감지 동작count() > 0이면 중복 생성 방지@Configuration
@RequiredArgsConstructor
public class BaseInitData {
@Autowired
@Lazy
private BaseInitData self;
private final PostService postService;
@Bean
ApplicationRunner baseInitDataApplicationRunner() {
return args -> {
self.work1();
};
}
@Transactional
public void work1() {
if (postService.count() > 0) return;
Post post1 = postService.write("제목 1", "내용 1");
Post post2 = postService.write("제목 2", "내용 2");
Post post3 = postService.write("제목 3", "내용 3");
post1.addComment("댓글 1-1");
post1.addComment("댓글 1-2");
post1.addComment("댓글 1-3");
post2.addComment("댓글 2-1");
post2.addComment("댓글 2-2");
}
}
self.work1() 형태로 호출할까?@Transactional은 “프록시” 기반으로 동작하는데,
같은 클래스 내부에서 this.work1()처럼 호출하면 프록시가 끼어들지 못해서 트랜잭션이 적용되지 않는 경우가 있습니다.
그래서 self(프록시 객체)를 주입받아 self.work1()로 호출해
트랜잭션이 정상 적용되도록 만든 패턴입니다.
Post ↔ PostComment 관계를 만들었고,다음(4강)에서는 엔티티를 그대로 JSON으로 응답할 때 발생하는
Jackson 순환참조 문제를 직접 확인하고 해결하게 된다.