나를 돌아보기
어제도 시간분배에 실패했다..!
Part. Spring
김영한님의 JPA 프로그래밍
양방향 연관관계 설정 코드
public void testORM_양방향_리펙토링(){
Team team1 = new Team("team1","팀1");
em.persist(team1);
Member member1 = new Member("member1","회원1");
member1.setTeam(team1); // 양방향 설정
em.persist(member1);
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); // 양방향 설정
em.persist(member2);
}
한번에 양방향 관계를 설정하는 메소드를 연관관계 편의 메소드라 한다.
연관관계 편의 메소드 작성 시 주의사항
setTeam()메소드에는 버그가 존재한다.
member1.setTeam(teamA);
member1.setTeam(teamB);
Member findMember = teamA.getMember(); // member1이 여전히 조회된다.
teamB로 변경시 teamA -> member1 관계를 제거하지 않아서 발생하는 문제이다.
연관관계를 변경할 때는 기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 한다.
기존 관계 제거
public void setTeam(Team team){
if(this.team != null){
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers.add(this);
}
관계를 변경하고 영속성 컨텍스트가 아직 살아있는 상태에서 teamA의 getMembers()를 호출하면 member1이 반환된다는 점이다.
변경된 연관관계는 앞서 설명한 것처럼 관계를 제거하는 것이 안전하다.
정리
단방향 매핑과 비교해서 양방향 매핑은 복잡
왜? 연관관계의 주인, 두 개의 단방향 연관관계를 양방향으로 만들기 위해 로직도 잘 관리해야 한다.
양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것뿐이다.
주인의 반대편은 mappedBy로 주인을 지정해야 한다.
주인의 반대편은 단순히 보여주는 일(객체 그래프 탐색)만 할 수 있다.
- 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다.
- 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.
- 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.
연관관계의 주인을 정하는 기준
단방향은 항상 외래 키가 있는 곳을 기준으로 매핑하면 된다. 양방향은 연관관계의 주인이라는 이름으로 오해가 있을 수 있다.
비즈니스 로직상 더 중요하다고 연관관계의 주인으로 선택하는 것이 아니라, 단순히 외래 키 관리자 정도의 의미만 부여해야 한다.
결론 : 연관관계의 주인은 외래 키의 위치와 관련해서 정해야지 비즈니스 중요도로 접근하면 안된다.
6. 다양한 연관관계 매핑
엔티티의 연관관계를 매핑할 때는 3가지를 고려해야 한다.
(1) 다중성
(2) 단방향, 양방향
(3) 연관관계의 주인
연관관계의 다중성
(1) 다대일(@ManyToOne)
(2) 일대다(@OneToMany)
(3) 일대일(@OneToOne)
(4) 다대다(@ManyToMany)
다중성을 판단하기 어려울 때는 반대방향을 생각해보면 된다.
보통 다대일과 일대다 관계를 가장 많이 사용하고 다대다 관계는 실무에서 거의 사용하지 않는다.
다대일
DB 테이블의 일대다 관계에서 외래 키는 항상 다쪽에 존재한다.
객체 양방향 관계에서 연관관계의 주인은 항상 다쪽이다.
다대일 단방향
회원 엔티티
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
//Getter,Setter...
}
팀 엔티티
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
//Getter, Setter ...
...
}
다대일 양방향
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public void setTeam(Team team){ // 양방향 연관관계
this.team = team; // 팀 추가하고
if(!team.getMembers().contains(this)){ // 무한루프 방지
team.getMembers().add(this); // 멤버에 팀 추가
}
}
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<Member>();
public void addMember(Member member){ // 양방향 연관관계
this.members.add(member); // 멤버 추가하고
if(member.getTeam()!= this){ // 무한 루프 방지
member.setTeam(this);
}
}
//Getter, Setter ...
...
}
양방향은 외래 키가 있는 쪽이 연관관계의 주인이다.
양방향 연관관계는 항상 서로를 참조해야 한다.
편의 메소드는 한 곳에만 작성하거나 양쪽 다 작성할 수 있는데, 양쪽에 다 작성하면 무한루프에 빠지므로 주의해야 한다.
일대다
일대다 관계는 엔티티를 하나 이상 참조할 수 있으므로 자바 컬렉션인 Collection, List, Set, Map 중에 하나를 사용해야한다.
일대다 단방향
보통 자신이 매핑한 테이블의 외래 키를 관리하는데, 이 매핑은 반대쪽 테이블에 있는 외래 키를 관리한다.
일대닫 관계에서는 외래 키는 항상 다쪽 테이블에 있다. 하지만 다 쪽인 Member 엔티티는 외래 키를 매핑할 수 있는
참조 필드가 없다. 대신에 반대쪽인 Team 엔티티에만 참조 필드인 members가 있다. 따라서 반대편 테이블의 외래 키를 관리하는 특이한 모습이 나타난다.
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID") // MEMBER 테이블의 TEAM_ID(FK)
private List<Member> members = new ArrayList<>();
//Getter, Setter ...
}
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
// Getter, Setter ...
}
일대다 단방향 관계를 매핑할 때는 @JoinColumn을 명시해야 한다.
일대다 단방향 매핑의 단점
매핑한 객체과 관리하는 외래 키가 다른 테이블에 있다는 점이다.
본인 테이블에 외래 키가 있으면 엔티티의 저장과 연관관계 처리를 INSERT SQL 한 번으로 끝낼 수 있지만, 다른 테이블에 외래 키가 있으면 연관관계 처리르 위한 UPDATE SQL을 추가로 실행해야 한다.
public void testSave(){
Member member1 = new Member("member1");
Member member2 = new Member("member2");
Team team1 = new Team("team1");
team1.getMembers().add(member1);
team1.getMembers().add(member2);
em.persist(member1); // INSERT-member1
em.persist(member2); // INSERT-member2
em.persist(team1); // INSERT-team1, UPDATE-member1.fk, UPDATE-member2.fk
transaction.commit();
}
📍 트랜잭션 커밋시, 내부적으로 flush가 호출된다.
insert into Member (MEMBER_ID, username) values (null,?)
insert into Member (MEMBER_ID, username) values (null,?)
insert into Team (TEAM_ID, name) values (null, ?)
update Member set TEAM_ID=? where MEMBER_ID=?
update Member set TEAM_ID=? where MEMBER_ID=?
일대다 단방향 매핑보다는 다대일 양방항 매핑을 사용하자
일대다 단방향 매핑은 엔티티를 매핑한 테이블이 아닌 다른 테이블의 외래 키를 관리해야 한다.
다대일 양방향 매핑은 관리해야 하는 외래 키가 본인 테이블에 있다.
일대다 양방향
일대다 양방향 매핑은 존재하지 않는다.
다대일 양방향 매핑을 사용해야 한다.(일대다 양방향과 다대일 양방향은 사실 똑같은 말이다. 여기서는 왼쪽을 연관관게의 주인으로 가정해 분류했다. ex) 다대일이면 다(N)가 연관관계의 주인이다.)
양방향 매핑에서 @OneToMany는 연관관계의 주인이 될 수 없다.
일대다, 다대일 관계는 항상 다쪽에 외래키가 존재하기 때문이다.
@ManyToOne에는 mappedBy 속성이 존재하지 않는다.
일대다 매핑이 완전히 불가능 한것은 아니다.
일대다 단방향 매핑 반대편에 같은 외래 키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 하나 추가하면 된다.
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<Member>();
//Getter, Setter...
}
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) // 읽기 전용
private Team team;
//Getter, Setter ...
}
다대일 쪽은 Insertable = false, updatable = false로 설정해서 읽기만 가능하게 했다.
일대다 단방향 매핑 반대편에 다대일 단방향 매핑을 읽기 전용으로 추가해서 일대다 양방향처럼 보이도록 하는 방법이다.
일대다 단방향 매핑이 가지는 단점을 그대로 가진다. 될 수 있으면 다대일 양방향 매핑을 사용하라!!
일대일
- 양쪽이 서로 하나의 관계만 가진다.
- 일대일 관계는 주 테이블이나 대상 테이블 둘 중 어느 곳이나 외래 키를 가질 수 있다.
(1) 주 테이블에 외래키
외래 키를 객체 참조와 비슷하게 사용할 수 있어서 객체지향 개발자들이 선호한다.
장점 : 주 테이블이 외래 키를 가지고 있으므로 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있다.
(2) 대상 테이블 외래키
전통적인 DB 개발자들은 보통 대상 테이블에 외래 키를 두는 것을 선호
장점 : 테이블의 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있다.
주 테이블에 외래키
단방향
MEMBER : 주 테이블
LOCKER : 대상 테이블
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
}
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
}
양방향
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
...
}
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne(mappedBy = "locker")
private Member member;
}
양방향이므로 연관관계의 주인을 정해야 한다. MEMBER 테이블이 외래 키를 가지고 있으므로 Member 엔티티에 있는
Member.locker가 연관관계의 주인이다. 반대 매핑인 사물함의 Locker.member는 mappedBy를 선언해서 연관관계의 주인이 아니라고설정했다.
대상 테이블에 외래키
단방향
일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않는다.
양방향
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne(mappedBy = "member")
private Locker locker;
...
}
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
}
일대일 매핑에서 대상 테이블에 외래 키를 두고 싶으면 양방향으로 매핑한다.
주 엔티티인 Member 엔티티 대신에 대상 엔티티인 Locker를 연관관계의 주인으로 만들어서 Locker 테이블의 외래 키를 관리하도록 한다.
다대다
관계형 DB는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다.
Member_Product 연결 테이블을 사용한다.
다대일 -> 일대다, 다대일 관계로 풀어내기
객체는 테이블과 다르게 객체 2개로 다대다 관계를 만들 수 있다.
회원 객체는 컬렉션을 사용해서 상품들을 참조하면 되고, 반대로 상품들도 컬렉션을 사용해서 회원들을 참조하면 된다.
다대다:단방향
다대다 단방향 회원
@Entity
public class Member {
@Id @Column(name = "MEMBER_ID")
private String id;
private String username;
@ManyToMany
@JoinTable(name = "MEMBER_PRODUCT",
joinColumns = @JoinColumn(name = "MEMBER_ID),
inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))
private List<Product> products = new ArrayList<Product>();
}
다대다 단방향 상품
@Entity
public class Product {
@Id @Column(name = "PRODUCT_ID")
private String id;
private String name;
...
}
회원 엔티티와 상품 엔티티를 @ManyToMany로 매핑했다.
@ManyToMany와 @JoinTable을 사용해서 연결 테이블을 바로 매핑한 것이다.
회원과 상품을 연결하는 회원_상품(Member_Product) 엔티티 없이 매핑을 완료할 수 있다.
@JoinTable 속성
- @JoinTable.name : 연결 테이블을 지정한다.
- @JoinTable.joinColumns : 현재 방향인 회원과 매핑할 조인 컬럼 정보를 지정한다.
- @JoinTable.inverseJoinColumns : 반대 방향인 상품과 매핑할 조인 컬럼 정보를 지정한다.
다대다 관계 저장
public void save(){
Product productA = new Product();
productA.setId("productA");
productA.setName("상품A");
em.persist(productA);
Member member1 = new Member();
member1.setId("member1");
member1.setUsername("회원1");
member1.getProducts().add(productA); // 연관관계 설정
em.persist(member1);
}
실행되는 SQL
INSERT INTO PRODUCT ...
INSERT INTO MEMBER ...
INSERT INTO MEMBER_PRODUCT ...
다대다 관계 탐색
public void find(){
Member member = em.find(Member.class, "member1");
List<Product> products = member.getProducts(); // 객체 그래프 탐색
for(Product product: products){
System.out.println("product.name = " + product.getName());
}
}
member.getProducts()를 호출해서 상품 이름을 출력하면 실행되는 SQL
SELECT * FROM MEMBER_PRODUCT MP
INNER JOIN PRODUCT P ON MP.PRODUCT_ID = P.PROUDCT_ID
WHERE MP.MEMBER_ID=?
다대다:양방향
역방향도 @ManyToMany를 사용한다.
양쪽 중 원하는 곳에 mappedBy로 연관관계 주인을 지정한다.
@Entity
public class Product {
@Id
private String id;
@ManyToMany(mappedBy = "products") // 역방항 추가
private List<Member> members;
}
다다대의 양방향 연관관계
member.getProducts().add(product);
product.getMembers().add(member);
양방향 연관관계는 연관관계 편의 메소드를 추가해서 관리하는 것이 편리하다.
public void addProduct(Product product){
products.add(product);
products.getMembers().add(this);
}
member.addProduct(product);
양방향 연관관계를 만들었으므로 product.getMembers()를 사용해서 역방향으로 객체 그래프를 탐색할 수 있다.
public void findInverse(){
Product product = em.find(Product.class, "productA");
List<Member> members = product.getMembers();
for(Member member: members){
System.out.println("member = " + member.getUsername());
}
}
다대다: 매핑의 한계와 극복, 연결 엔티티 사용
ex) 회원이 상품을 주문하면 연결 테이블에 단순히 주문한 회원 아이디와 상품 아이디만 담고 끝나지 않는다.
보통은 연결 테이블에 주문 수량 컬럼이나 주문한 날짜 같은 컬럼이 더 필요하다.
연결 테이블에 주문 수량, 주문 날짜 컬럼을 추가했다.
이렇게 컬럼을 추가하면 더는 @ManyToMany를 사용할 수 없다.
주문 엔티티나 상품 엔티티에는 추가한 컬럼들을 매핑할 수 없기 때문이다.
연결 테이블을 매핑하는 연결 엔티티를 만들고 이곳에 추가한 컬럼들을 매핑해야 한다.
엔티티의 관계도 테이블 관계처럼 다대다에서 일대다, 다대일 관계로 풀어야 한다.
회원 엔티티
@Entity
public class Member {
@Id @Column(name = "MEMBER_ID")
private String id;
// 역방향
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts;
}
회원상품 엔티티 쪽이 외래 키를 가지고 있으므로 연관관계의 주인이다.
상품 엔티티
@Entity
public class Product {
@Id @Column(name = "PRODUCT_ID")
private String id;
private String name;
}
상품 엔티티에서 회원상품 엔티티로 객체 그래프 탐색 기능이 필요하지 않다고 생각해서 연관관계를 만들지 않았다.
회원 상품 엔티티
@Entity
@IdClass(MemberProductId.class)
public class MemberProduct {
@Id
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member; // MemberProductId.member와 연결
@Id
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product; // MemberProductId.product와 연결
private int orderAmount;
}
회원상품 식별자 클래스
public class MemberProductId implements Serializable {
private String member; // MemberProduct.member와 연결
private String product; // MemberProduct.product와 연결
// hashCode and equals
@Override
public boolean equals(Object o) {...}
@Override
public int hashCode() {...}
}
기본 키를 매핑하는 @Id와 외래 키를 매핑하는 @JoinColumn을 동시에 사용해서 기본 키 + 외래 키를 한번에 매핑했다.
@IdClass를 사용해서 복합 기본 키를 매핑했다.
복합 기본키
회원상품 엔티티는 기본 키가 MEMBER_ID와 PRODUCT_ID로 이루어진 복합 기본키다.
JPA에서 복합 키를 사용하려면 별도의 식별자 클래스를 만들어야 한다.
엔티티에 @IdClass를 사용해서 식별자 클래스를 지정하면 된다.
- 복합 키는 별도의 식별자 클래스로 만들어야 한다.
- Serializable을 구현해야 한다.
- equals와 hashCode 메소드를 구현해야 한다.
- 기본 생성자가 있어야 한다.
- 식별자 클래스는 public이어야 한다.
- @IdClass를 사용하는 방법 외에 @EmbeddedId를 사용하는 방법도 있다.
식별 관계
회원 상품은 회원과 상품의 기본 키를 받아서 자신의 기본 키로 사용한다.
이렇게 부모 테이블의 기본키를 받아서 자신의 기본 키 + 외래 키로 사용하는 것을 데이터베이스 용어로 식별 관계라 한다.
회원상품은 회원의 기본 키를 받아서 자신의 기본 키로 사용함과 동시에 회원관의 관계를 위한 외래 키로 사용한다.
상품의 기본 키도 받아서 자신의 기본 키로 사용함과 동시에 상품과의 관계를 위한 외래 키로 사용한다.
MemberProductId 식별자 클래스로 두 기본 키를 묶어서 복합 기본 키로 사용한다.
저장하는 코드
public void save() {
Member member1 = new Member();
member1.setId("member1");
member1.setUsername("회원1");
em.persist(member1);
Product productA = new Product();
productA.setId("productA");
productA.setName("상품1");
em.persist(productA);
MemberProduct memberProduct = new MemberProduct();
memberProduct.setMember(member1);
memberProduct.setProduct(productA);
memberProduct.setOrderAmount(2);
em.persist(memberProduct);
}
조회하는 코드
pubilc void find(){
MemberProductId memberProductId = new MemberProductId();
memberProductId.setMember("member1");
memberProductId.setProduct("productA");
MemberProduct memberProduct = em.find(MemberProduct.class, memberProductId);
Member member = memberProduct.getMember();
Product product = memberProduct.getProduct();
}
복합 키는 항상 식별자 클래스를 만들어야 한다.
em.find()를 보면 생성한 식별자 클래스로 엔티티를 조회한다.
복합키를 사용하면 복합 키를 위한 식별자 클래스도 만들어야 하고 @IdClass 또는 @EmbeddedId도 사용해야 한다.
다대다 : 새로운 기본 키 사용(복합키 사용 x)
추천하는 기본 키 생성 전략 : 데이터베이스에서 자동으로 생성해주는 대리 키를 Long 값으로 사용하는 것
장점 : 간편하고 영구히 쓸 수 있으며, 비즈니스에 의존하지 않는다.
주문 코드
@Entity
public class Order {
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
private int orderAmount;
}
대리 키를 사용함으로써 이전에 보았던 식별 관계에 복합 키를 사용하는 것보다 매핑이 단순하고 이해하기 쉽다.
회원 엔티티, 상품 엔티티 코드
@Entity
public class Member {
@Id @Column(name = "MEMBER_ID")
private String id;
private String username;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<Order>();
}
@Entity
public class Product {
@Id @Column(name = "PRODUCT_ID")
private String id;
private String name;
}
저장 코드
public void save(){
// 회원 저장
Member member1 = new Member();
member1.setId("member1");
member1.setUsername("회원1");
em.persist(member1);
// 상품 저장
Product productA = new Product();
productA.setId("productId");
productA.setName("상품1");
em.persist(productA);
// 주문 저장
Order order = new Order();
order.setMember(member1);
order.setProduct(product);
order.setOrderAmount(2);
em.persist(order);
}
조회 코드
public void find(){
Long orderId = 1L;
Order order = em.find(Order.class,orderId);
Member member = order.getMember();
Product product = order.getProduct();
}
식별자 클래스를 사용하지 않아서 코드가 한결 더 단순해졌다.
새로운 기본 키를 사용해서 다대다 관계를 풀어내는 것도 좋은 방법이다.
다대다 연관관계 정리
- 식별 관계 : 받아온 식별자를 기본 키 + 외래 키로 사용한다.
- 비식별 관계 : 받아온 식별자는 외래 키로만 사용하고 새로운 식별자를 추가한다.
비식별 관계를 사용하는 것이 복합 키를 위한 식별자 클래스를 만들지 않아도 되므로 단순하고 편리하게 ORM 매핑을 할 수 있다.
식별 관계보다는 비식별 관계를 추천한다..!
토비의 스프링(vol.1)
초난감 DAO
DAO
DAO(Data Access Object)는 DB를 사용해 데이터를 조회하거나 조작하는 긴으을 전담하도록 만든 오브젝트를 말한다.
User
사용자 정보를 저장할 User 클래스를 만든다.
package com.example.tobispring.오브젝트와의존관계;
public class User {
String id;
String name;
String password;
public String getId(){
return id;
}
public void setId(String id){
this.id = id;
}
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
public String getPassword(){
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
SQL(테이블 생성)
create table users (
id varchar(10) primary key,
name varchar(20) not null,
password varchar(10) not null
)
1.1 UserDao
사용자 정보를 DB에 넣고 관리할 수 있는 DAO클래스
JDBC를 이용하는 작업의 일반적인 순서
(1) DB 연결을 위한 Connection을 가져온다.
(2) SQL을 담은 Statement(또는 PreparedStatement)를 만든다.
(3) 만들어진 Statement를 실행한다.
(4) 조회의 경우 SQL 쿼리의 실행 결과를 ResultSet으로 받아서 정보를 저장할 오브젝트(User)에 옮겨준다.
(5) 작업 중에 생성된 Connection, Statement, ResultSet 같은 리소스는 작업을 마친 후 반드시 닫아준다.
(6) JDBC API가 만들어내는 예외를 잡아서 직접 처리하거나, 메소드에 throws를 선언해서 예외가 발생하면 메소드 밖으로 던지게 한다.
UserDao
package com.example.tobispring.오브젝트와의존관계.dao;
import com.example.tobispring.오브젝트와의존관계.domain.User;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver"); // ClassNotFoundException
// 컴파일 타임에 직접적인 참조 없이 런타임에 동적으로 클래스를 로드하기 위함
Connection c = DriverManager.getConnection( // SQLException
"jdbc:mysql://localhost:3306/tobyspring","root",""
);
PreparedStatement ps = c.prepareStatement(
"insert into users(id,name,password) values(?,?,?)"
);
ps.setString(1,user.getId());
ps.setString(2,user.getName());
ps.setString(3,user.getPassword());
ps.executeUpdate();
ps.close();
c.close();
}
public User get(String id) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/tobyspring","root","");
PreparedStatement ps = c.prepareStatement(
"select * from users where id = ?"
);
ps.setString(1,id);
ResultSet rs = ps.executeQuery();
rs.next();
User user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
rs.close();
ps.close();
c.close();
return user;
}
}
TestCode - main()을 이용한
package com.example.tobispring.오브젝트와의존관계.test;
import com.example.tobispring.오브젝트와의존관계.dao.UserDao;
import com.example.tobispring.오브젝트와의존관계.domain.User;
import java.sql.SQLException;
public class UserDaoTest {
public static void main(String[] args) throws SQLException, ClassNotFoundException {
UserDao dao = new UserDao();
User user = new User();
user.setId("hello");
user.setName("sangyunpark");
user.setPassword("123123");
dao.add(user);
System.out.println(user.getId() + "등록 성공");
User user2 = dao.get(user.getId());
System.out.println(user2.getName());
}
}
출력 결과
hello등록 성공
sangyunpark
테스트 결과
이렇게 만든 UserDao 클래스 코드에는 여러가지 문제가 존재한다.
1.2 DAO의 분리
1.2.1 관심사 분리
변화는 대체로 집중된 한 가지 관심에 대해서만 일어나지만 그에 따른 작업은 한 곳에 집중되지 않는 경우가 많다.
우리가 준비해야 할 것은 할 일은 한가지 관심이 한 군데에 집중되게 하는 것이다.
관심이 같은 것끼리는 모으고, 관심이 다른 것은 따로 떨어져 있게 하는 것이다.
관심사의 분리란?
관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이게 하고, 관심이 다른 것은 가능한 따로 떨어져서 서로 영향을 주지 않도록 분리해야 한다.
1.2.2 커넥션 만들기 추출
(1) DB와 연결을 위한 커넥션을 어떻게 가져올까라는 관심
(2) 사용자 등록을 위해 DB에 보낼 SQL문장을 담을 Statement를 만들고 실행하는 것
(3) 작업이 끝나면 사용한 리소스인 Statement와 Connection 오브젝트를 닫아줘서 소중한 공유 리소스를 시스템에 돌려주는 것
관심사가 방만하게 중복되어 있고, 여기저기 흩어져 있어서 다른 관심의 대상과 얽혀 있으면, 변경이 일어날 때 엄청난 고통을 일으키는 원인이 된다. 지저분하게 꼬여 있는 스파게티 코드가 된다!
중복 코드의 메소드 추출(리팩토링 - 메소드 추출 기법)
private Connection getConnection() throws ClassNotFoundException, SQLException {
// 컴파일 타임에 직접적인 참조 없이 런타임에 동적으로 클래스를 로드하기 위함
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/tobyspring", "root", ""
);
return c;
}
Connection c = getConnection();
UserDao 클래스의 메소드가 2,000개쯤 된다고 상상해보면, DB 연결과 관련도니 부분에 변경이 일어났을 경우, DB 종류와 접속 방법이 바뀌어서 드라이버 클래스와 URL이 바뀌었다거나, 로그인 정보가 변경돼도 앞으로는 getConnection()이라는 메소드의 코드만 수정하면 된다.
관심의 종류에 따라 코드를 구분해놓았기 때문에 한 가지 관심에 대한 변경이 일어날 경우 그 관심이 집중되는 부분의 코드만 수정하면 된다.
1.2.2 DB 커넥션 만들기의 독립
변화를 반기는 DAO를 만들어보기
DB커넥션을 가져오는 데 있어 독자적으로 만든 방법을 적용하고 싶어할 경우
고객에게 소스를 직접 공개하고 싶지는 않은 경우
고객에게 미리 컴파일된 클래스 바이너리 파일만 제공하고 싶은 경우
UserDao 소스코드를 제공해주지 않고 고객 스스로 원하는 DB 커넥션 생성 방식을 적용해가면서 UserDao를 사용하게 할 수 있을까?
상속을 통한 확장
기존 UserDao 코드를 한 단계 더 분리하면 된다.
UserDao에서 메소드의 구현 코드를 제거하고 getConnection()을 추상 메소드로 만든다.
public abstract Connection getConnection() throws ClassNotFoundException,SQLException;
메소드의 구현은 제거되고 추상 메소드로 변경되었다. 메소드의 구현은 서브 클래스가 담당한다.
package com.example.tobispring.오브젝트와의존관계.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class NUserDao extends UserDao{
@Override
public Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/tobyspring", "root", ""
);
return c;
}
}
슈퍼클래스에 기본적인 로직의 흐름을 만들고, 그 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드 등으로 만든 뒤 서브 클레스에서 이런 메소드를 필요에 맞게 구현해서 사용하도록 하는 방법을 디자인 패턴에서 템플릿 메소드 패턴이라고 한다.
서브 클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것을 팩토리 메소드 패턴이라고 부르기도 한다.
위의 모든 내용에 달하는 설명을 한 문장으로 정의하면,
"UserDao에 팩토리 메소드 패턴을 적용해서 getConnection()을 분리합시다"라는 한마디에 담을 수 있다.
위 방법은 상속을 사용했다는 단점이 존재한다.
이미 UserDao가 다른 목적을 위해 상속을 사용하고 있다면 어쩔 것일까?
자바는 클래스의 다중 상속을 허용하지 않는다.
여전히 상속관계는 두 가지 다른 관심사에 대해 긴밀한 결합을 허용한다.
다른 DAO 클래스들이 계속 만들어진다면 그때는 상속을 통해서 만들어진 getConnection()의 구현 코드가 매 DAO 클래스마다 중복돼서 나타난다.
1.3 DAO의 확장
1.3.1 클래스의 분리
완전히 독립적인 클래스로 만들어보자..!
DB 커넥션과 관련된 부분을 서브 클래스가 아니라, 아예 별도의 클래스(SimpleConnectionMaker)에 담는다.
이렇게 만든 클래스를 UserDao가 이용하게 하면 된다.
SimpleConnectionMaker를 매번 새로 만드는 것보다는 한 번만 SimpleConnectionMaker 오브젝트를 만들어서 저장해두고 이를 계속 사용하는 편이 낫다..
SimpleConnectionMaker
package com.example.tobispring.오브젝트와의존관계.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class SimpleConnectionMaker {
public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/tobyspring", "root", ""
);
return c;
}
}
UserDao
package com.example.tobispring.오브젝트와의존관계.dao;
import com.example.tobispring.오브젝트와의존관계.domain.User;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class UserDao {
private SimpleConnectionMaker simpleConnectionMaker;
public UserDao(){
simpleConnectionMaker = new SimpleConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = simpleConnectionMaker.makeNewConnection();
PreparedStatement ps = c.prepareStatement(
"insert into users(id,name,password) values(?,?,?)"
);
ps.setString(1,user.getId());
ps.setString(2,user.getName());
ps.setString(3,user.getPassword());
ps.executeUpdate();
ps.close();
c.close();
}
public User get(String id) throws ClassNotFoundException, SQLException {
Connection c = simpleConnectionMaker.makeNewConnection();
PreparedStatement ps = c.prepareStatement(
"select * from users where id = ?"
);
ps.setString(1,id);
ResultSet rs = ps.executeQuery();
rs.next();
User user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
rs.close();
ps.close();
c.close();
return user;
}
}
테스트 성공
악! N사와 D사에 UserDao 클래스만 공급하고 상속을 통해 DB 커넥션 기능을 확장해서 사용하게 했던 부분이 다시 불가능해졌다..
UserDao가 SimpleConnectionMaker라는 특정 클래스와 그 코드에 종속적이기 때문에 앞으로 납품 후 고객이 DB 커넥션을 가져오는 방법을 자유롭게 확장하기가 힘들어졋다.
1.3.1 인터페이스의 도입
두 개의 클래스가 서로 긴밀하게 연결되어 있지 않도록 중간에 추상적인 느슨한 연결고리를 만들어주는 것이다.
추상화란 어떤 것들의 공통적인 성격을 뽑아내어 이를 따로 분리해내는 작업이다.
인터페이스는 어떤 일을 하겠다는 기능만 정의해놓은 것이다.
따라서 인터페이스에는 어떻게 하겠다는 구현 방법은 나타나 있지 않다. 그것은 인터페이스를 구현할 클래스들이 알아서 결정할 일이다.
ConnectionMaker 구현 클래스
package com.example.tobispring.오브젝트와의존관계.dao;
import java.sql.Connection;
import java.sql.SQLException;
public class NConnectionMaker implements ConnectionMaker{
@Override
public Connection makeConnection() throws ClassNotFoundException, SQLException {
// N사의 독자적인 방법으로 Connection을 생성하는 코드
return null;
}
}
ConnectionMaker 인터페이스를 사용하도록 개선한 UserDao
package com.example.tobispring.오브젝트와의존관계.dao;
import com.example.tobispring.오브젝트와의존관계.domain.User;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class UserDao {
private ConnectionMaker connectionMaker; // 인터페이스를 통해 오브젝트에 접근하므로 구체적인 클래스 정보를 알 필요가 없다.
public UserDao(){
connectionMaker = new NConnectionMaker(); // 여기는 클래스 이름이 나온다!
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = connectionMaker.makeConnection(); // 인터페이스에 정의된 메소드를 사용하므로 클래스가 바뀐다고 해도 메소드 이름이 변경될 걱정이 없다.
PreparedStatement ps = c.prepareStatement(
"insert into users(id,name,password) values(?,?,?)"
);
ps.setString(1,user.getId());
ps.setString(2,user.getName());
ps.setString(3,user.getPassword());
ps.executeUpdate();
ps.close();
c.close();
}
public User get(String id) throws ClassNotFoundException, SQLException {
Connection c = connectionMaker.makeConnection();
PreparedStatement ps = c.prepareStatement(
"select * from users where id = ?"
);
ps.setString(1,id);
ResultSet rs = ps.executeQuery();
rs.next();
User user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
rs.close();
ps.close();
c.close();
return user;
}
}
📍 NConnection 클래스의 생성자를 호출해서 오브젝트를 생성하는 코드가 여전히 UserDao에 남아 있다.
connectionMaker = new NConnectionMaker(); // 여기는 클래스 이름이 나온다!
제거하고 싶지만 간단한 방법이 없다!
클래스 이름을 넣어서 오브젝트를 만들지 않으면 어떻게 해야할까?
Java
Chatper 12. 지네릭스, 열거형, 애너테이션
1.1 지네릭스?
지네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능이다.
객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.
타입의 안정성을 높인다는 것?
의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄어준다는 뜻
제너릭스의 장점
1. 타입 안정성을 제공한다.
2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.
타입을 미리 명시해줌으로써 번거로운 형변환을 줄여준다.
1.2 지네릭 클래스의 선언
지네릭 타입은 클래스와 메서드에 선언할 수 있다.
클래스 옆에 <T>를 붙이면 된다.
package com.example.javabible.ch12_지네릭스;
public class Box<T> {
T item;
void setItem(T item){
this.item = item;
}
T getItem(){
return item;
}
}
BOX<T>에서 T를 타입 변수라고 하며, Type의 첫 글자에서 따온 것이다.
타입 변수는 T가 아닌 다른 것을 사용해도 된다. ArrayList<E>의 경우, 타입 변수 E는 'Element(요소)'의 첫 글자를 따서 사용했다.
오늘도 정신 똑띠 차려야하는데...
시간분배를 잘 하지 못했다..!
내일 다시 시간을 잘 관리하고 공부하며 나아가자!
나에게 아쉬운 점
열정은 넘치나 시간활용을 제대로 하지 못하는 점이 너무 아쉽다.
내가 하고 싶은 공부량만큼 시간도 아껴야하는데, 밸런스가 잘 맞지 않는다.
좋은 기업에 가고싶은 만큼 더 열심히 해야하는데, 자꾸 합리화하며 타협한다.
하나님께 시간을 잘 관리할 수 있는 지혜달라고 구하고, 지금 현재 주어진 상황에 최선을 다하는 연습을 해야겠다..!
완벽하게 시간을 컨트롤 할 수는 없으니, 내 계획대로 안되었다고 해도 좌절하지 말고, 그 순간부터 최선을 다하는 연습을 하자!
하기 싫은 근본적인 이유는 새로운것에 대한 두려움인것 같다. 그 두려움을 버리고 하나님한테 더 공부할 수 있는 용기를 달라고 기도하자!
'Backend > 공부 일지' 카테고리의 다른 글
2024/03/29(금) (1) | 2024.03.30 |
---|---|
2024/03/28(목) (1) | 2024.03.29 |
2024/03/13(수) (3) | 2024.03.14 |
2024/03/12(화) (0) | 2024.03.13 |
2024/03/11(월) (2) | 2024.03.12 |