✅ MyBatis의 단점
1️⃣ SQL 쿼리 작성
MyBatis를 통해 SQL 쿼리를 JDBC를 사용했을 때 보다 더 쉽게 작성할 수 있었지만, 어쨋든 여전히 SQL 쿼리를 개발자가 직접 작성해야했다.
<!-- insert 메서드에 대한 매핑 -->
<insert id="insert" parameterType="org.example.recipe_board.domain.Recipe" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
INSERT INTO recipe (writer_id, food_name, ingredients, process)
VALUES (#{writerId}, #{foodName}, #{ingredients}, #{process});
</insert>
<!-- update 메서드에 대한 매핑 -->
<update id="update" parameterType="org.example.recipe_board.domain.Recipe">
UPDATE recipe
<set>
<if test="foodName != null and foodName.length() > 0">food_name = #{foodName},</if>
<if test="writerId != null">writer_id = #{writerId},</if>
<if test="ingredients != null and ingredients.length() > 0">ingredients = #{ingredients},</if>
<if test="process != null and process.length() > 0">process = #{process},</if>
<if test="readCount != null">read_count = #{readCount},</if>
</set>
WHERE id = #{id};
</update>
2️⃣ 자바 객체와 데이터베이스 테이블 간의 패러다임 불일치
Member와 Recipe가 1 대 다 관계 이고 Recipe가 Member를 기본키에 대해 외래키 참조할 때,
테이블에는
Member가 Recipe에 대한 리스트를 갖고 있지 않지만
CREATE TABLE member (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
service_id VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL
);
CREATE TABLE recipe (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
food_name VARCHAR(255) NOT NULL,
ingredients TEXT, -- 'ingridents'에서 'ingredients'로 수정
process TEXT,
writer_id BIGINT,
read_count INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (writer_id) REFERENCES member(id) ON DELETE CASCADE ON UPDATE CASCADE
);
객체에는
Member가 Recipe에 대한 리스트를 갖고 있어야 함
public class Member {
private Long id;
private String serviceId;
private String password;
private List<Recipe> recipes;
}
public class Recipe {
private Long id;
private Long writerId;
private String foodName;
private String ingredients;
private String process;
private Integer readCount;
private String createdAt;
}
그렇기 때문에 특정 Member에 대한 Recipe를 자바에 가져오려면 조인을 하거나, 두 번 쿼리를 실행해서 매핑을 시켜야 했다.
여기에 Image, Comment까지 추가되면, 매우매우 복잡해진다.
public class Member {
private Long id;
private String serviceId;
private String password;
private List<Recipe> recipes;
private List<Comment> comments;
}
public class Recipe {
private Long id;
private Long writerId;
private String foodName;
private String ingredients;
private String process;
private Integer readCount;
private String createdAt;
private List<Image> images;
private List<Comment> comments;
}
public class Comment {
private Long id;
private Long parentId;
private Long recipeId;
private Long writerId;
private String content;
private String createdAt;
}
public class Image {
private Long id;
private Long recipeId;
private String originalName;
private String savedPath;
}
✅ JPA의 도입
JPA란?
💡 Java Persistence API의 약자로 Java 진영에서 표준으로 사용되는 ORM 인터페이스이다.
ORM이란?
💡 Object Relational Mapping의 약자로 말 그대로 객체와 관계형 데이터베이스의 테이블을 매핑시켜주는 기술을 의미한다.

기존에는 개발자가 직접 쿼리를 작성해 JDBC에 전달했다면
JPA를 사용하면 개발자는 단순히 객체만 전달하면, 알아서 JPA가 쿼리를 만들어 JDBC에게 전달시켜준다.
결국, JPA도 내부적으로는 JDBC를 사용한다는 점을 명심해야 한다.
JPA를 쓰는 이유
1. SQL 작성을 안해도 됨
앞서도 잠깐 언급했지만 SQL을 개발자가 직접할 필요가 없어진다.
심지어 자바코드로 엔티티를 작성하면 알아서 DDL까지 만들어준다.
[DDL 생성 예시]
@Entity
public class Image {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "recipe_id", nullable = false)
private Recipe recipe;
@Column(name = "original_name", length = 100)
private String originalName;
@Column(columnDefinition = "TEXT")
private String savedPath;
}
→
CREATE TABLE image (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
recipe_id BIGINT,
original_name VARCHAR(100),
saved_path TEXT,
FOREIGN KEY (recipe_id) REFERENCES recipe(id) ON DELETE CASCADE ON UPDATE CASCADE
);
2. 객체와 테이블 간의 패러다임 차이 극복
JPA를 사용하는 가장 큰 이유는 객체와 테이블을 매핑시켜준다는 것이다.
앞서 언급했듯, MyBatis는 객체와 테이블의 패러다임이 다르기 때문에 서로 연관관계에 있을 시 개발자가 직접 객체에 일일이 매핑을 시켜줘야 했다.
JPA를 통해 몇가지 어노테이션만 활용하면 이러한 매핑도 자동으로 시켜준다.
[JPA가 없을 때]
public class Member {
private Long id;
private String serviceId;
private String password;
private List<Recipe> recipes;
}
public class Recipe {
private Long id;
private Long writerId;
private String foodName;
private String ingredients;
private String process;
private Integer readCount;
private String createdAt;
}
[JPA가 있을 때]
@Entity
@Table(name = "member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "service_id", nullable = false, unique = true)
private String serviceId;
@Column(nullable = false)
private String password;
@OneToMany(mappedBy = "writer", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Recipe> recipes;
}
@Entity
@Table(name = "recipe")
public class Recipe {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "writer_id", nullable = false)
private Member writer;
@Column(name = "food_name", nullable = false)
private String foodName;
@Column(columnDefinition = "TEXT")
private String ingredients;
@Column(columnDefinition = "TEXT")
private String process;
@Column(name = "read_count")
private Integer readCount = 0;
@Column(name = "created_at", updatable = false)
private String createdAt = String.valueOf(LocalDateTime.now());
}
- 특히 @OneToMany, @ManyToOne 어노테이션을 사용하면 하나의 테이블에 대해서만 쿼리를 날려도 연관된 테이블의 정보까지 다 끌고 온다.
- 예를 들어 Member 테이블에서 특정 id로 Member를 조회하면 단순히 Member 테이블의 컬럼만 갖고 오는 것이 아니라, 연관된 Recipe들까지 같이 끌고온다.
- 이를 통해 귀찮게 조인을 하거나, 두 번 쿼리를 날려 매핑시켜주는 일을 하지 않아도 된다.
✅ 구현하기
1️⃣ 의존성 추가
Spring Data JPA를 활용했다.
물론 Spring이 관여하지 않는 순수 JPA만을 사용할 수 있지만 편리함을 위해 Spring Data JPA를 활용했다.
build.gradle
// jpa추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
2️⃣ @Entity 정의
JPA를 사용할 때 가장 먼저 해야하는 일은 테이블과 매핑시킬 객체를 정의하는 것이다.
1. Member
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "service_id", nullable = false, unique = true)
private String serviceId;
@Column(nullable = false)
private String password;
@OneToMany(mappedBy = "writer", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Recipe> recipes;
@OneToMany(mappedBy = "writer", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Comment> comments;
public Member(String serviceId, String password) {
this.serviceId = serviceId;
this.password = password;
}
}
2. Recipe
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "recipe")
public class Recipe {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "writer_id", nullable = false)
private Member writer;
@Column(name = "food_name", nullable = false)
private String foodName;
@Column(columnDefinition = "TEXT")
private String ingredients;
@Column(columnDefinition = "TEXT")
private String process;
@Column(name = "read_count")
private Integer readCount = 0;
@Column(name = "created_at", updatable = false)
private String createdAt = String.valueOf(LocalDateTime.now());
@OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Image> images;
@OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Comment> comments;
public Recipe(Long id, Long writerId, String foodName, String ingredients, String process) {
this.id = id;
this.writer = new Member();
writer.setId(writerId);
this.foodName = foodName;
this.ingredients = ingredients;
this.process = process;
}
}
3. Image
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Image {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "recipe_id", nullable = false)
private Recipe recipe;
@Column(name = "original_name", length = 100)
private String originalName;
@Column(columnDefinition = "TEXT")
private String savedPath;
}
4. Comment
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "comment")
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "parent_id")
private Comment parent;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Comment> replies = new ArrayList<>();
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "recipe_id", nullable = false)
private Recipe recipe;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "writer_id", nullable = false)
private Member writer;
@Column(length = 300)
private String content;
@Column(name = "created_at", updatable = false)
private String createdAt = LocalDateTime.now().toString();
public Comment(Long id, Comment parent, Recipe recipe, Member writer, String content, String createdAt) {
this.id = id;
this.parent = parent;
this.recipe = recipe;
this.writer = writer;
this.content = content;
this.createdAt = createdAt;
}
}
3️⃣ Repository
1. MemberRepository
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByServiceId(String serviceId);
}
2. RecipeRepository
@Repository
public interface RecipeRepository extends JpaRepository<Recipe, Long> {
@Query("select r from Recipe r")
List<Recipe> selectPageList(Pageable pageable);
}
3. ImageRepository
@Repository
public interface ImageRepository extends JpaRepository<Image, Long> {
}
4. CommentRepository
@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
List<Comment> findByRecipeId(Long id);
}
✅ 느낀점
JPA를 도입하니 확실히 리포지토리 코드가 눈에 띄게 줄었고 객체와 테이블 매핑도 해주다 보니 비즈니스 로직도 간결해졌다. JPA에 대한 이해도는 아직 많이 부족하지만 어떤 느낌인지 정도는 파악할 수 있는 활동이었다.
JPA관련해서 공부해야할 키워드가 많은데 앞으로 하나하나 학습해 나갈 계획이다.
✅ 소스코드
simple_board/very-simple-board_ver7 at main · jhk01007/simple_board
Contribute to jhk01007/simple_board development by creating an account on GitHub.
github.com
'데브코스 > 실습 & 프로젝트' 카테고리의 다른 글
[1차 프로젝트] 코드리뷰 피드백 반영하기 (0) | 2024.09.09 |
---|---|
[1차 프로젝트] 기본키를 UUID로 할 때 주의점과 JPA 엔티티의 기본키 타입 고민 (1) | 2024.09.07 |
게시판 만들기 - 기능 추가: 댓글(대댓글) 기능 추가 (0) | 2024.08.30 |
게시판 만들기 - 기능추가: Pagination(페이지네이션) (0) | 2024.08.27 |
게시판 만들기 - 데이터 계층 리팩토링(MyBatis 도입) (0) | 2024.08.26 |
✅ MyBatis의 단점
1️⃣ SQL 쿼리 작성
MyBatis를 통해 SQL 쿼리를 JDBC를 사용했을 때 보다 더 쉽게 작성할 수 있었지만, 어쨋든 여전히 SQL 쿼리를 개발자가 직접 작성해야했다.
<!-- insert 메서드에 대한 매핑 -->
<insert id="insert" parameterType="org.example.recipe_board.domain.Recipe" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
INSERT INTO recipe (writer_id, food_name, ingredients, process)
VALUES (#{writerId}, #{foodName}, #{ingredients}, #{process});
</insert>
<!-- update 메서드에 대한 매핑 -->
<update id="update" parameterType="org.example.recipe_board.domain.Recipe">
UPDATE recipe
<set>
<if test="foodName != null and foodName.length() > 0">food_name = #{foodName},</if>
<if test="writerId != null">writer_id = #{writerId},</if>
<if test="ingredients != null and ingredients.length() > 0">ingredients = #{ingredients},</if>
<if test="process != null and process.length() > 0">process = #{process},</if>
<if test="readCount != null">read_count = #{readCount},</if>
</set>
WHERE id = #{id};
</update>
2️⃣ 자바 객체와 데이터베이스 테이블 간의 패러다임 불일치
Member와 Recipe가 1 대 다 관계 이고 Recipe가 Member를 기본키에 대해 외래키 참조할 때,
테이블에는
Member가 Recipe에 대한 리스트를 갖고 있지 않지만
CREATE TABLE member (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
service_id VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL
);
CREATE TABLE recipe (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
food_name VARCHAR(255) NOT NULL,
ingredients TEXT, -- 'ingridents'에서 'ingredients'로 수정
process TEXT,
writer_id BIGINT,
read_count INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (writer_id) REFERENCES member(id) ON DELETE CASCADE ON UPDATE CASCADE
);
객체에는
Member가 Recipe에 대한 리스트를 갖고 있어야 함
public class Member {
private Long id;
private String serviceId;
private String password;
private List<Recipe> recipes;
}
public class Recipe {
private Long id;
private Long writerId;
private String foodName;
private String ingredients;
private String process;
private Integer readCount;
private String createdAt;
}
그렇기 때문에 특정 Member에 대한 Recipe를 자바에 가져오려면 조인을 하거나, 두 번 쿼리를 실행해서 매핑을 시켜야 했다.
여기에 Image, Comment까지 추가되면, 매우매우 복잡해진다.
public class Member {
private Long id;
private String serviceId;
private String password;
private List<Recipe> recipes;
private List<Comment> comments;
}
public class Recipe {
private Long id;
private Long writerId;
private String foodName;
private String ingredients;
private String process;
private Integer readCount;
private String createdAt;
private List<Image> images;
private List<Comment> comments;
}
public class Comment {
private Long id;
private Long parentId;
private Long recipeId;
private Long writerId;
private String content;
private String createdAt;
}
public class Image {
private Long id;
private Long recipeId;
private String originalName;
private String savedPath;
}
✅ JPA의 도입
JPA란?
💡 Java Persistence API의 약자로 Java 진영에서 표준으로 사용되는 ORM 인터페이스이다.
ORM이란?
💡 Object Relational Mapping의 약자로 말 그대로 객체와 관계형 데이터베이스의 테이블을 매핑시켜주는 기술을 의미한다.

기존에는 개발자가 직접 쿼리를 작성해 JDBC에 전달했다면
JPA를 사용하면 개발자는 단순히 객체만 전달하면, 알아서 JPA가 쿼리를 만들어 JDBC에게 전달시켜준다.
결국, JPA도 내부적으로는 JDBC를 사용한다는 점을 명심해야 한다.
JPA를 쓰는 이유
1. SQL 작성을 안해도 됨
앞서도 잠깐 언급했지만 SQL을 개발자가 직접할 필요가 없어진다.
심지어 자바코드로 엔티티를 작성하면 알아서 DDL까지 만들어준다.
[DDL 생성 예시]
@Entity
public class Image {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "recipe_id", nullable = false)
private Recipe recipe;
@Column(name = "original_name", length = 100)
private String originalName;
@Column(columnDefinition = "TEXT")
private String savedPath;
}
→
CREATE TABLE image (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
recipe_id BIGINT,
original_name VARCHAR(100),
saved_path TEXT,
FOREIGN KEY (recipe_id) REFERENCES recipe(id) ON DELETE CASCADE ON UPDATE CASCADE
);
2. 객체와 테이블 간의 패러다임 차이 극복
JPA를 사용하는 가장 큰 이유는 객체와 테이블을 매핑시켜준다는 것이다.
앞서 언급했듯, MyBatis는 객체와 테이블의 패러다임이 다르기 때문에 서로 연관관계에 있을 시 개발자가 직접 객체에 일일이 매핑을 시켜줘야 했다.
JPA를 통해 몇가지 어노테이션만 활용하면 이러한 매핑도 자동으로 시켜준다.
[JPA가 없을 때]
public class Member {
private Long id;
private String serviceId;
private String password;
private List<Recipe> recipes;
}
public class Recipe {
private Long id;
private Long writerId;
private String foodName;
private String ingredients;
private String process;
private Integer readCount;
private String createdAt;
}
[JPA가 있을 때]
@Entity
@Table(name = "member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "service_id", nullable = false, unique = true)
private String serviceId;
@Column(nullable = false)
private String password;
@OneToMany(mappedBy = "writer", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Recipe> recipes;
}
@Entity
@Table(name = "recipe")
public class Recipe {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "writer_id", nullable = false)
private Member writer;
@Column(name = "food_name", nullable = false)
private String foodName;
@Column(columnDefinition = "TEXT")
private String ingredients;
@Column(columnDefinition = "TEXT")
private String process;
@Column(name = "read_count")
private Integer readCount = 0;
@Column(name = "created_at", updatable = false)
private String createdAt = String.valueOf(LocalDateTime.now());
}
- 특히 @OneToMany, @ManyToOne 어노테이션을 사용하면 하나의 테이블에 대해서만 쿼리를 날려도 연관된 테이블의 정보까지 다 끌고 온다.
- 예를 들어 Member 테이블에서 특정 id로 Member를 조회하면 단순히 Member 테이블의 컬럼만 갖고 오는 것이 아니라, 연관된 Recipe들까지 같이 끌고온다.
- 이를 통해 귀찮게 조인을 하거나, 두 번 쿼리를 날려 매핑시켜주는 일을 하지 않아도 된다.
✅ 구현하기
1️⃣ 의존성 추가
Spring Data JPA를 활용했다.
물론 Spring이 관여하지 않는 순수 JPA만을 사용할 수 있지만 편리함을 위해 Spring Data JPA를 활용했다.
build.gradle
// jpa추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
2️⃣ @Entity 정의
JPA를 사용할 때 가장 먼저 해야하는 일은 테이블과 매핑시킬 객체를 정의하는 것이다.
1. Member
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "service_id", nullable = false, unique = true)
private String serviceId;
@Column(nullable = false)
private String password;
@OneToMany(mappedBy = "writer", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Recipe> recipes;
@OneToMany(mappedBy = "writer", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Comment> comments;
public Member(String serviceId, String password) {
this.serviceId = serviceId;
this.password = password;
}
}
2. Recipe
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "recipe")
public class Recipe {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "writer_id", nullable = false)
private Member writer;
@Column(name = "food_name", nullable = false)
private String foodName;
@Column(columnDefinition = "TEXT")
private String ingredients;
@Column(columnDefinition = "TEXT")
private String process;
@Column(name = "read_count")
private Integer readCount = 0;
@Column(name = "created_at", updatable = false)
private String createdAt = String.valueOf(LocalDateTime.now());
@OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Image> images;
@OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Comment> comments;
public Recipe(Long id, Long writerId, String foodName, String ingredients, String process) {
this.id = id;
this.writer = new Member();
writer.setId(writerId);
this.foodName = foodName;
this.ingredients = ingredients;
this.process = process;
}
}
3. Image
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Image {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "recipe_id", nullable = false)
private Recipe recipe;
@Column(name = "original_name", length = 100)
private String originalName;
@Column(columnDefinition = "TEXT")
private String savedPath;
}
4. Comment
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "comment")
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "parent_id")
private Comment parent;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Comment> replies = new ArrayList<>();
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "recipe_id", nullable = false)
private Recipe recipe;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "writer_id", nullable = false)
private Member writer;
@Column(length = 300)
private String content;
@Column(name = "created_at", updatable = false)
private String createdAt = LocalDateTime.now().toString();
public Comment(Long id, Comment parent, Recipe recipe, Member writer, String content, String createdAt) {
this.id = id;
this.parent = parent;
this.recipe = recipe;
this.writer = writer;
this.content = content;
this.createdAt = createdAt;
}
}
3️⃣ Repository
1. MemberRepository
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByServiceId(String serviceId);
}
2. RecipeRepository
@Repository
public interface RecipeRepository extends JpaRepository<Recipe, Long> {
@Query("select r from Recipe r")
List<Recipe> selectPageList(Pageable pageable);
}
3. ImageRepository
@Repository
public interface ImageRepository extends JpaRepository<Image, Long> {
}
4. CommentRepository
@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
List<Comment> findByRecipeId(Long id);
}
✅ 느낀점
JPA를 도입하니 확실히 리포지토리 코드가 눈에 띄게 줄었고 객체와 테이블 매핑도 해주다 보니 비즈니스 로직도 간결해졌다. JPA에 대한 이해도는 아직 많이 부족하지만 어떤 느낌인지 정도는 파악할 수 있는 활동이었다.
JPA관련해서 공부해야할 키워드가 많은데 앞으로 하나하나 학습해 나갈 계획이다.
✅ 소스코드
simple_board/very-simple-board_ver7 at main · jhk01007/simple_board
Contribute to jhk01007/simple_board development by creating an account on GitHub.
github.com
'데브코스 > 실습 & 프로젝트' 카테고리의 다른 글
[1차 프로젝트] 코드리뷰 피드백 반영하기 (0) | 2024.09.09 |
---|---|
[1차 프로젝트] 기본키를 UUID로 할 때 주의점과 JPA 엔티티의 기본키 타입 고민 (1) | 2024.09.07 |
게시판 만들기 - 기능 추가: 댓글(대댓글) 기능 추가 (0) | 2024.08.30 |
게시판 만들기 - 기능추가: Pagination(페이지네이션) (0) | 2024.08.27 |
게시판 만들기 - 데이터 계층 리팩토링(MyBatis 도입) (0) | 2024.08.26 |