[데브코스] Spring Boot REST API 실습 (2·3강)

zuno·2025년 12월 19일

UI 방식 선택(Thymeleaf vs React) + JPA/Validation 추가 + 샘플 데이터 생성

이번 글은 2강과 3강 내용을 한 번에 정리합니다.

  • 2강: UI를 Thymeleaf(서버 렌더링)로 할지, React(클라이언트 렌더링)로 할지에 따라
    스프링부트 구현 방식이 어떻게 달라지는지
  • 3강: Validation / JPA 추가, 그리고 샘플 데이터(글 3개 + 댓글) 자동 생성까지

2강 — UI 구현 방식에 따라 백엔드 구조가 달라진다

✅ 뷰(View)는 UI다

UI를 어디서 렌더링하느냐에 따라 구조가 갈립니다.

  • Thymeleaf / JSP 방식
    스프링부트 “안”에 뷰(템플릿)가 포함
    → 서버가 HTML을 만들어서 내려줌(서버 사이드 렌더링)

  • React / Vue / Svelte 방식
    → UI가 스프링부트 “밖”에 존재
    → 서버는 데이터(JSON)만 제공하고, 화면은 프론트 앱이 렌더링(클라이언트 사이드 렌더링)


✅ 2강 핵심 정리: “데이터 가공”의 위치가 바뀐다

1) Thymeleaf로 UI 구현할 때

  • 백엔드가 템플릿에 넣을 데이터를 가공해서 내려줘야 함
  • 컨트롤러는 주로 @Controller
  • 반환값은 뷰 이름(viewName) 이고, Model에 값을 담음

예시(개념):

@Controller
public class PostViewController {
    @GetMapping("/posts")
    public String posts(Model model) {
        model.addAttribute("posts", postService.findAll());
        return "posts"; // posts.html
    }
}

✅ 장점

  • 서버에서 HTML까지 다 만들기 때문에 구조가 단순할 수 있음
  • 작은 프로젝트/관리자 페이지에 빠르게 적용 가능

2) React로 UI 구현할 때

  • 백엔드는 데이터(JSON)만 제공하면 됨
  • 컨트롤러는 주로 @RestController
  • 반환값은 Java 객체 → Jackson이 JSON으로 변환

예시(개념):

@RestController
@RequestMapping("/api/v1/posts")
public class ApiV1PostController {
    @GetMapping
    public List<Post> posts() {
        return postService.findAll();
    }
}

✅ 장점

  • 다양한 클라이언트(웹/모바일/외부서비스)에서 API 재사용 가능
  • 프론트/백 분리 협업에 유리

✅ 결론 (2강 한 줄 요약)

UI가 스프링부트 내부(Thymeleaf)에 있으면 서버가 HTML을 렌더링하고,
UI가 외부(React)에 있으면 서버는 JSON만 제공한다.


3강 — Validation/JPA 추가 + 샘플 데이터 생성

3강에서는 본격적으로 “게시글 + 댓글” 도메인을 만들기 위해
JPA 엔티티 / 레포지토리 / 서비스를 구성하고,
실습 편의를 위해 샘플 데이터를 자동 생성하도록 설정합니다.


✅ 1) BaseEntity: 공통 필드(id, 생성/수정 시간) 구성

실습에서는 여러 엔티티가 공통으로 가져야 하는 필드(예: id, 생성/수정 시간)를
BaseEntity로 묶어두는 패턴을 사용합니다.

  • @MappedSuperclass: 상속받는 엔티티에 필드가 포함되도록
  • @EntityListeners(AuditingEntityListener.class): Auditing 활성화
  • @CreatedDate, @LastModifiedDate: 생성/수정 시간 자동 기록

그리고 BackApplication에서 @EnableJpaAuditing을 켜줘야 auditing이 동작합니다.


✅ 2) Post / PostComment 엔티티 구성 (양방향 관계)

Post (글)

  • 글은 댓글 리스트를 가짐 (@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;
    }
}

PostComment (댓글)

  • 댓글은 글을 참조함 (@ManyToOne)
  • 즉, Post(1) : PostComment(N) 관계
@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로 해결하게 됩니다.


✅ 3) Repository / Service 구성

Repository

JpaRepository<Post, Integer>로 기본 CRUD 사용

public interface PostRepository extends JpaRepository<Post, Integer> {
}

Service

서비스는 레포지토리를 이용해 글 작성/조회 등을 담당합니다.

@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();
    }
}

✅ 4) 샘플 데이터 자동 생성 (글 3개 + 댓글 생성)

서버 실행 시 데이터가 없으면 자동으로 넣어주도록 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()로 호출해
트랜잭션이 정상 적용되도록 만든 패턴입니다.


🔚 2강 + 3강 정리

  • 2강: UI를 어디서 렌더링하느냐(Thymeleaf vs React)에 따라
    백엔드가 HTML을 만들지(JSON만 주는지) 구조가 달라진다.
  • 3강: JPA/Validation 기반을 깔고,
    Post ↔ PostComment 관계를 만들었고,
    서버 실행 시 샘플 데이터(글 3개 + 댓글)를 자동 생성하도록 구성했다.

다음(4강)에서는 엔티티를 그대로 JSON으로 응답할 때 발생하는
Jackson 순환참조 문제를 직접 확인하고 해결하게 된다.

0개의 댓글