728x90

예외 처리

JPA표준 예외들은 javax.persistence.PersistenceException의 자식 클래스다. 

그리고 이 예외 클래스는 RuntimeException의 자식이다.

따라서 JPA예외는 모두 언체크 예외다.

JPA 표준 예외는 크게 2가지로 나눌 수 있다.

  • 트랜잭션 롤백을 표시하는 예외
  • 트랜젝션 롤백을 표시하지 않는 예외

트랜젝션 롤백을 표시하는 예외는 심각한 예외이므로 복구해선 안된다.

출처 : 자바 ORM 표준 JPA 프로그래밍

 

스프링 프레임워크의 JPA 예외 변환

서비스 계층에서 데이터 접근 계층의 구현 기술에 직접 의존하는 것은 좋은 설계가 아니듯, 예외 또한 마찬가지이다.

스프링 프레임워크는 이런 문제를 해결하려고 데이터 접근 계층에 대한 예외를 추상화해서 개발자에게 제공한다.

출처 : 자바 ORM 표준 JPA 프로그래밍

스프링 프레임워크에 JPA 예외 변환기 적용

JPA 예외를 스프링 프레임워크가 제공하는 추상화 된  예외로 변경하려면 PersistenceExceptionTranslationPostProcesser를 스프링 빈으로 등록하면 된다.

이것은 @Repository 어노테이션을 사용한 곳에 예외 변환 AOP를 적용해서 JPA예외를 스프링 프레임워크가 추상화한 예외로 변환해준다.

 

@Repository
public class NoResultExceptionTestRepository
{
    @PersistenceContext
    EntityManager em;
    
    public Member findMember()
    {
        // 조회된 데이터가 없음
        return em.createQuery("select m from Member m", Member.class)
            .getSingleResult();
    }
}

이 메소드는 조회된 결과가 없으면 javax.persistence.NoResultException이 발생한다.

이 예외가 메소드를 빠져나갈 때 PersistenceExceptionTranslationPostProcessor에서 등록한 AOP 인터셉터가 동작해서 해당 예외를 org.springframework.dao.EmptyResultDataAccessException예외로 변환해서 반환한다.

@Repository
public class NoResultExceptionTestRepository
{
    @PersistenceContext
    EntityManager em;
    
    public Member findMember() throws javax.persistence.NoResultException
    {
        // 조회된 데이터가 없음
        return em.createQuery("select m from Member m", Member.class)
            .getSingleResult();
    }
}

만약 예외를 변환하지 않고 그대로 반환하고 싶으면 위 코드처럼 throws 절에 명시하면 된다.

 

트랜잭션 롤백 시 주의사항

트랜잭션을 롤백하는 것은 DB의 반영사항만 롤백하는 것이지 수정한 자바 객체까지 원상태로 복구해주지는 않는다.

따라서 트랜잭션이 롤백된 영속성 컨텍스트를 그대로 사용하는 것은 위험하다.

새로운 영속성 컨텍스트를 생성해서 사용하거나 EntityManager.clear()를 호출해서 영속성 컨텍스트를 초기화한 다음에 사용해야 한다.

 

기본 전략인 트랜잭션당 영속성 컨텍스트 전략은 문제가 발생하면 트랜잭션 AOP 종료 시점에 트랜잭션을 롤백하면서 영속성 컨텍스트도 함꼐 종료하므로 문제가 발생하지 않는다.

문제는 OSIV처럼 영속성 컨텍스트의 범위를 트랜잭션 범위보다 넓게 사용해서 여러 트랜잭션이 하나의 영속성 컨텍스트를 사용할 때 발생한다.

이때는 트랜잭션을 롤백해서 영속성 컨텍스트에 이상이 발생해도 다른 트랜잭션에서 해당 영속성 컨텍스트를 그대로 사용하는 문제가 있다.

스프링 프레임워크는 영속성 컨텍스트의 범위를 트랜잭션의 범위보다 넓게 설정하면 잘못된 영속성 컨텍스트를 사용하는 문제를 예방한다.

 

엔티티 비교

영속성 컨텍스트 내부에는 엔티티 인스턴스를 보관하기 위한 1차 캐시가 있다.

영속성 컨텍스트를 더 정확히 이해하기 위해서는 1차 캐시의 가장 큰 장점인 애플리케이션 수준의 반복 가능한 읽기를 이해해야한다.

Member member1 = em.find(Member.class, "1L");
Member member2 = em.find(Member.class, "2L");

assertTrue(member1 == member2); // 둘은 같은 인스턴스다.

같은 영속성 컨텍스트에서 엔티티를 조회하면 위 코드와 같이 항상 같은 엔티티 인스턴스를 반환한다.

 

영속성 컨텍스트가 같을 때 엔티티 비교

 

출처 : 자바 ORM 표준 JPA 프로그래밍

@Transactional
public class MemberServiceTest
{
    @Autowired
    MemberService memberService;
    
    @Autowired
    MemberRepository memberRepository;
    
    @Test
    pulbic void 회원가입() throws Exception
    {
        //Given
        Member member = new Member("kim");
        
        //When
        Long saveId = memberService.join(member);
        
        //Then 
        Member findMember = memberRepository.fineOne(saveId);
        assertTrue(member == findMember); // 참조값 비교
    }
}

위 코드의 테스트는 트랜잭션 안에서 시작하므로 테스트의 범위와 트랜잭션의 범위가 위 그림과 같다.

따라서 테스트 전체에서 같은 영속성 컨텍스트에 접근한다.

 

위 코드와 같이 테스트 클래스의 @Transactional이 선언되어 있으면 트랜잭션을 먼저 시작하고 테스트 메소드를 실행한다.

그러므로 회원가입 메서드에서 사용된 코드는 항상 같은 트랜잭션과 같은 영속성 컨텍스트에 접근한다.

 

출처 : 자바 ORM 표준 JPA 프로그래밍

저장한 회원과 회원 리포지토리에서 찾아온 엔티티가 완전히 같은 인스턴스이다.

이것은 같은 트랜잭션 범위에 있으므로 같은 영속성 컨텍스트를 사용하기 때문이다.

따라서 영속성 컨텍스트가 같으면 엔티티를 비교할 때 다음 3가지 조건을 모두 만족한다.

  • 동일성(identical): == 비교가 같다.
  • 동등성(equinalent): equals() 비교가 같다.
  • DB 동등성 : @Id인 DB 식별자가 같다.

영속성 컨텍스트가 다를 때 엔티티 비교

// @Transactional
public class MemberServiceTest
{
    @Autowired
    MemberService memberService;
    
    @Autowired
    MemberRepository memberRepository;
    
    @Test
    pulbic void 회원가입() throws Exception
    {
        //Given
        Member member = new Member("kim");
        
        //When
        Long saveId = memberService.join(member);
        
        //Then 
        Member findMember = memberRepository.fineOne(saveId);
        assertTrue(member == findMember); // 참조값 비교
    }
}

위 코드는 실패한다.

  1. 테스트 코드에서 memberService.join(member)를 호출해서 회원가입을 시도하면 서비스 계층에서 트랜잭션이 시작되고, 영속성 컨텍스트1이 만들어진다.
  2. memberRepository 에서 em.persist()를 호출해서 member 엔티티를 영속화한다.
  3. 서비스 계층이 끝날 때 트랜잭션이 커밋되면서 영속성 컨텍스트가 플러시된다. 이때 트랜잭션과 영속성 컨텍스트가 종료된다.
  4. 테스트 코드에서 memberRepository.findOne(saveId)를 호출해서 저장한 엔티티를 조회하면 리포지토리 계층에서 새로운 트랜잭션이 시작되면서 새로운 영속성 컨텍스트2가 생성된다.
  5. 저장된 회원을 조회하지만 새로 생성된 영속성 컨텍스트 2에는 찾는 회원이 존재하지 않는다.
  6. 따라서 DB에서 찾아온다.

member와 findMember는 각각 다른 영속성 컨텍스트에서 관리되었기 때문에 둘은 다른 인스턴스다.

하지만 member와 findMember는 인스턴스는 다르지만 같은 DB 로우를 가르키고 있어서 사실상 같은 엔티티이다.

  • 동일성(identical): == 비교가 실패한다.
  • 동등성(equinalent): equals() 비교가 만족한다. 단 equals를 구현해야한다.
  • DB 동등성: @Id인 DB 식별자가 같다.

지금처럼 영속성 컨텍스트가 달라지면 동일성 비교는 실패하기 때문에 다른 방법을 사용해야 한다.

 

프록시 심화 주제

프록시는 원본 엔티티를 상속받아서 만들어지므로 엔티티를 사용하는 클라이언트는 엔티티가 프록시인지 원본엔티티인지 구분하지 않고 사용할 수 있다.

 

영속성 컨텍스트와 프록시

영속성 컨텍스트는 자신이 관리하는 영속 엔티티의 동일성(identical)을 보장한다.

@Test
public void 영속성컨텍스트와_프록시()
{
    Member newMember = new Member("member1", "회원1");
    em.persist(newMember);
    em.flush();
    em.clear();
    
    Member refMember = em.getReference(Member.class, "member1");
    Member findMember = em.find(Member.class, "member1");
    
    Assert.assertTrue(refMember == findMember); //성공
}

refMember는 프록시고 findMember는 원본 엔티티이므로 둘은 서로 다른 인스턴스로 생각할 수 있지만 이렇게 되면 영속성 컨텍스트가 영속 엔티티의 동일성을 보장하지 못하는 문제가 발생한다.

때문에 영속성 콘텍스트는 프록시로 조회된 엔티티에 대해서 같은 엔티티를 찾는 요청이 오면 원본 엔티티가 아닌 처음 조회된 프록시를 반환한다.

따라서 프록시로 조회해도 영속성 컨텍스트는 영속 엔티티의 동일성을 보장한다.

 

@Test
public void 영속성컨텍스트와_프록시()
{
    Member newMember = new Member("member1", "회원1");
    em.persist(newMember);
    em.flush();
    em.clear();
    
    Member findMember = em.find(Member.class, "member1");
    Member refMember = em.getReference(Member.class, "member1");
    
    Assert.assertTrue(refMember == findMember); //성공
}

반대로 원본 엔티티를 먼저 조회하고 나서 프록시를 조회했다.

원본 엔티티가 조회되었는데 프록시를 반환할 필요가 없다.

따러서 em.getReference()를 호출해도 프록시가 아닌 원본을 반환한다.

 

프록시 타입 비교

@Test
public void 프록시_타입비교() {
    Member newMember = new Member("member1", "회원1");
    em.persist(newMember);
    em.flush();
    em.clear();

    Member refMember = em.getReference(Member.class, "member1");
    System.out.println("refMember Type = " + refMember.getClass());

    Assert.assertFalse(Member.class == refMember.getClass()); // false
    Assert.assertTrue(refMember instanceof Member); // true
}

프록시는 원본 엔티티를 상속 받아서 만들어지므로 프록시로 조회한 엔티티의 타입을 == 비교하면 안되고 대신에  instanceof를 사용해야 한다.

==비교는 부모 클래스와 자식 클래스를 == 비교한 것이 되서 false이다.

 

프록시 동등성 비교

엔티티의 동등성을 비교하려면 비즈니스 키를 사용해서 equals()메소드를 오버라이딩하고 비교하면 된다.

그러나 IDE나 외부 라이브러리를 사용해서 구현한 equals() 메소드로 엔티티를 비교할 때, 비교 대상이 원본 엔티티면 문제가 없지만 프록시면 문제가 발생할 수 있다.

@Entity
public class Member {

    @Id
    private String id;
    private String name;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;

        if (this.getClass() != obj.getClass()) return false; //1

        Member member = (Member) obj;

        if (name != null ? !name.equals(member.name) : member.name != null) //2
            return false;

        return true;
    }

    @Override
    public int hashCode() {
        return name != null ? name.hashCode() : 0;
    }
}

회원 엔티티는 name필드를 비즈니스 키로 사용해서 equals() 메서드를 오버라이딩했다.

@Test
public void 프록시와_동등성비교()
{
    Member saveMember = new Member("member1", "회원1");
    em.persists(saveMember);
    em.flush();
    em.clear();
    
    Member newMember = new Member("member1", "회원1");
    Member refMember = em.getReference(Member.class, "member1");
    
    Assert.assertTrue( newMember.equals(refMember)); 
}

 

 

출처 : 자바 ORM 표준 JPA 프로그래밍

새로 생성한 회원 newMember와 프록시로 조회한 회원 refMember의 name 속성은 둘 다 회원1로 같으므로 동등성 비교를 하면 성공할 것 같다.

하지만 실행해보면 false가 나오면서 테스트가 실패한다.

 

this.getClass() != obj.getClass()) return false;

여기서 타입을 동일성(==) 비교한다.

앞서 이야기한 대로 프록시는 원본을 상속받은 자식 타입이므로 프록시의 타입을 비교할 때는 == 비교가 아닌 instanceof를 사용해야 한다. 

 

출처 : 자바 ORM 표준 JPA 프로그래밍

Member member = (Member) obj; // member는 프록시다.

if (name != null ? !name.equals(member.name):member.name != null)
    return false;

equals() 메서드를 구현할 때는 일반적으로 멤버변수를 직접 비교하는데, 프록시의 경우는 문제가 된다.

프록시는 실제 데이터를 가지고 있지 않기 때문이다.

따라서 프록시의 멤버변수에 직접 접근하면 아무 값도 조회할 수 없다.

따라서 equals()는 false를 반환한다.

 

Member member = (Member) obj;

if(name != null ? !name.equals(member.getName()) : member.getName() != null)
    return false;

다음은 수정한 코드다.

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof Member)) return false;

    Member member = (Member) obj;
    if (name != null ? !name.equals(member.getName()) : member.getName() != null)
        return false;

    return true;
}

 

정리

  • 프록시의 타입 비교는 ==비교 대신에 instanceof를 사용해야 한다.
  • 프록시의 멤버변수에 직접 접근하면 안 되고 대신에 접근자 메소드를 사용해야 한다.

상속관계와 프록시

 

출처 : 자바 ORM 표준 JPA 프로그래밍

@Test
public void 부모타입으로_프록시조회() {

    // 테스트 데이터 준비
    Book saveBook = new Book();
    saveBook.setName("jpaBook");
    saveBook.setAuthor("kim");
    em.persist(saveBook);

    em.flush();
    em.clear();

    // 테스트 시작
    Item proxyItem = em.getReference(Item.class, saveBook.getId());
    System.out.println("proxyItem = " + proxyItem.getClass());

    if (proxyItem instanceof Book) {
        System.out.println("proxyItem instanceof Book");
        Book book = (Book) proxyItem;
        System.out.println("책 저자 = " + book.getAuthor());
    }

    // 결과 검증
    Assert.assertFalse(proxyItem.getClass() == Book.class);
    Assert.assertFalse(proxyItem instanceof Book);
    Assert.assertTrue(proxyItem instanceof Item);
}

위 코드는 Item을 조회해서 Book 타입이면 저자 이름을 출력한다.

 

코드 분석

먼저 em.getReference() 메소드를 사용해서 Item엔티티를 프록시로 조회했다.

그리고 instanceof 연산을 사용해서 proxyItem이 Book 클래스 타입인지 확인한다.

Book 타입이면 다운캐스팅해서 Book 타입으로 변경하고 저자 이름을 출력한다.

 

그런데 출력 결과를 보면 기대와는 다르게 저자가 출력되지 않은 것을 알 수 있다.

 

출처 : 자바 ORM 표준 JPA 프로그래밍

코드에서는 em.getReference() 메서드를 사용해서 Item 엔티티를 프록시로 조회했다.

이때 실제 조회된 엔티티는 Book 이므로 Book 타입을 기반으로 원본 엔티티 인스턴스가 생성된다.

그런데 em.getReference() 메서드에서 Item엔티티를 대상으로 조회했으므로 프록시인 proxyItem은 Item타입을 기반으로 만들어진다.

이 프록시 클래스는 원본 엔티티로 Book 엔티티를 참조한다.

 

프록시인 proxyItem은 Item의 프록시 타입이고 이 타입은 Book타입과 관계가 없기 때문이다.

캐스팅 또한 되지 않는다.

 

해결 방법

 

1. JPQL로 대상 직접 조회

가장 간단한 방법은 다음과 같이 처음부터 자식타입을 직접 조회해서 필요한 연산을 하면 된다.

물론 이 방법을 사용하면 다형성을 활용할 수 없다.

Book jpqlBook = em.createQuery(
    "select b from Book b where b.id=:bookId", Book.class)
    .setParameter("bookId", item.getId())
    .getSingleResult();

 

2. 프록시 벗기기

하이버네이트가 제공하는 기능을 사용하면 프록시에서 원본 엔티티를 가져올 수 있다.

...
Item item = orderItem.getItem();
Item unProxyItem = unProxy(item);

if (unProxyItem instanceof Book) {
    System.out.println("proxyItem instanceof Book");
    Book book = (Book) unProxyItem;
    System.out.println("책 저자 = " + book.getAuthor());
}

Assert.assertTrue(item != unProxyItem);

// 하이버네이트가 제공하는 프록시에서 원본 엔티티를 찾는 기능을 사용하는 메소드
public static <T> T unProxy(Object entity) {
    if (entity instanceof HibernateProxy) {
        entity = ((HibernateProxy) entity)
            .getHibernateLazyInitializer()
            .getImplementation();
    }
    return (T) entity;
}

영속성 컨텍스트는 한 번 프록시로 노출한 엔티티는 계쏙 프록시로 노출한다.

그래야 영속성 컨텍스트가 영속 엔티티의 동일성을 보장할 수 있고, 클라이언트는 조회한 엔티티가 프록시인지 아닌지 구분하지 않고 사용할 수 있다.

하지만 이 방법은 프록시에서 원본 엔티티를 직접 꺼내기 때문에 프록시와 원본 엔티티의 동일성 비교가 실패한다는 문제점이 있다.

 

3. 기능을 위한 별도의 인터페이스 제공

출처 : 자바 ORM 표준 JPA 프로그래밍

public interface TitleView
{
    String getTitle();
}

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item implements TitleView {

    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name;
    private int price;
    private int stockQuantity;

    // Getter, Setter
}

@Entity
@DiscriminatorValue("B")
public class Book extends Item {

    private String author;
    private String isbn;

    @Override
    public String getTitle() {
        return "[제목:" + getName() + " 저자:" + author + "]";
    }
}

@Entity
@DiscriminatorValue("M")
public class Movie extends Item {

    private String director;
    private String actor;

    // Getter, Setter

    @Override
    public String getTitle() {
        return "[제목:" + getName() + " 감독:" + director + " 배우:" + actor + "]";
    }
}

위 코드에서 TitleView라는 공통 인터페이스를 만들고 자식 클래스들은 인터페이스의 getTitle() 메소드를 각각 구현했다.

@Entity
public class OrderItem {

    @Id @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ITEM_ID")
    private Item item;

    ...

    public void printItem() {
        System.out.println("TITLE=" + item.getTitle());
    }
}

이제 위 코드처럼 Item의 getTitle() 을 호출하면 된다.

 

이처럼 인터페이스를 제공하고 각각의 클래스가 자신에 맞는 기능을 구현하는 것은 다형성을 활용하는 좋은 방법이다.

이후에 다양한 상품 타입이 추가되어도 Item을 사용하는 OrderItem의 코드는 수정하지 않아도 된다.

그리고 이 방법은 클라이언트 입장에서 대상 객체가 프록시인지 아닌지를 고민하지 않아도 되는 장점이 있다.

 

4. 비지터 패턴 사용

 

출처 : 자바 ORM 표준 JPA 프로그래밍

위 그림을 보면 비지터 패턴은 Visitor와 Visitor를 받아들이는 대상 클래스로 구성된다.

여기서는 Item이 accept(visitor) 메서드를 사용해서 Visitor를 받아들인다.

그리고 Item은 단순히 Visitor를 받아들이기만 하고 실제 로직은 Visitor가 처리한다.

 

Visitor 인터페이스

public interface Visitor
{
    void visit(Book book);
    void visit(Album album);
    void visit(Movie movie);
}

Visitor에는 visit()라는 메소드를 정의하고 모든 대상 클래스를 받아들이도록 작성하면 된다.

여기서는 Book, Album, Movie를 대상 클래스로 사용한다.

 

비지터 구현

public class PrintVisitor implements Visitor {

    @Override
    public void visit(Book book) {
        // 넘어오는 book은 Proxy가 아닌 원본 엔티티다.
        System.out.println("book.class = " + book.getClass());
        System.out.println("[PrintVisitor] [제목:" + book.getName() + " 저자:" + book.getAuthor() + "]");
    }

    @Override
    public void visit(Album album) { ... }

    @Override
    public void visit(Movie movie) { ... }
}

public class TitleVisitor implements Visitor {

    private String title;

    public String getTitle() {
        return title;
    }

    @Override
    public void visit(Book book) {
        title = "[제목:" + book.getName() + " 저자:" + book.getAuthor() + "]";
    }

    @Override
    public void visit(Album album) { ... }

    @Override
    public void visit(Movie movie) { ... }
}

 

위 코드는 Visitor의 구현 클래스로 대상 클래스의 내용을 출력해주는 PrintVisitor와 대상 클래스의 제목을 보관하는 TitleVisitor를 작성했다.

 

비지터 대상 클래스

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {

    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name;

    ...

    public abstract void accept(Visitor visitor);
}

@Entity
@DiscriminatorValue("B")
public class Book extends Item {

    private String author;
    private String isbn;
    // Getter, Setter
    public String getAuthor() {
        return author;
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

@Entity
@DiscriminatorValue("M")
public class Movie extends Item {

    private String director;
    private String actor;
    // Getter, Setter

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

@Entity
@DiscriminatorValue("A")
public class Album extends Item {

    ...
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

위 코드에 있는 각각의 자식 클래스들은 부모에 정의한 accept(visitor)메서드를 구현했는데, 구현 내용은 단순히  파라미터로 넘어온 Visitor의 visit(this) 메서드를 호출하면서 자신(this)을 파라미터로 넘기는 것이 전부다.

 

@Test
public void 상속관계와_프록시_VisitorPattern() {
    ...
    OrderItem orderItem = em.find(OrderItem.class, orderItemId);
    Item item = orderItem.getItem();

    // PrintVisitor
    item.accept(new PrintVisitor());
}

실제로는 위 코드처럼 사용한다.

 

출처 : 자바 ORM 표준 JPA 프로그래밍

먼저 item.accept() 메서드를 호출하면서 파라미터로 PrintVisitor를 넘겨주었다.

item은 프록시이므로 먼저 프록시(ProxyItem)가 accept() 메서드를 받고 원본 엔티티(book)의 accept()를 실행한다.

원본 엔티티

다음 코드를 실행해서 자신(this)을 visitor에 파라미터로 넘겨준다.

visitor가 PrintVisitor 타입이므로 PrintVisitor.visit(this) 메서드가 실행되는데 이때 this가 Book타입이므로 visit(Book book) 메서드가 실행된다.

 

비지터 패턴 정리

 

장점

  • 프록시에 대한 걱정 없이 안전하게 원본 엔티티에 접근할 수 있다.
  • instanceof와 타입캐스팅 없이 코드를 구현할 수 있다.
  • 알고리즘과 객체 구조를 분리해서 구조를 수정하지 않고 새로운 동작을 추가할 수 있다.

단점

  • 너무 복잡하고 더블 디스패치를 사용해야하기 때문에 이해하기 어렵다.
  • 객체 구조가 변경되면 모든 Visitor를 수정해야 한다.

 

성능 최적화

 

N + 1 문제

JPA로 애플리케이션을 개발할 때 성능상 가장 주의해야 하는 것이 N + 1 문제다

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
    private List<Order> orders = new ArrayList<Order>();

    ...
}

 

@Entity
@Table(name = "ORDERS")
public class Order {

    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    private Member member;

    ...
}

 

회원과 주문정보는 1:N, N:1 양방향 연관관계다.

그리고 회원이 참조하는 주문정보인 Member.orders를 즉시로딩으로 설정했다.

 

즉시 로딩과 N+1

특정 회원 하나를 em.find() 메서드로 조회하면 즉시 로딩으로 설정한 주문 정보도 함께 조회한다.

 

여기서 함께 조회하는 방법이 중요한데, SQL을 두 번 실행하는 것이 아니라 조인을 사용해서 한번의 SQl로 회원과 주문정보를 함께 조회한다.

여기까지만 보면 즉시 로딩이 상당히 좋아보인다.

문제는 JPQL을 사용할 때 발생한다.

 

JPQL을 실행하면 JPA는 이것을 분석해서 SQL을 생성한다.

이때는 즉시 로딩과 지연 로딩에 대해서 전혀 신경 쓰지 않고 JPQL만 사용해서 SQL을 생성한다.

따라서 다음과 같은 SQL이 실행된다.

SELECT * FROM MEMBER

SQL의 실행 결과로 먼저 회원 엔티티를 애플리케이션에 로딩한다.

그런데 회원 엔티티와 연관된 주문 컬렉션이 즉시 로딩으로 설정되어 있으므로 JPA는 주문 컬렉션을 즉시 로딩하려고 다음 SQL 을 추가로 실행한다.

SELECT * FROM ORDERS WHERE MEMBER_ID = ?

조회된 회원이 하나면 이렇게 총 2번의 SQL을 실행하지만 조회된 회원이 5명이면 

SELECT * FROM MEMBER // 1번 실행으로 회원 5명 조회
SELECT * FROM ORDERS WHERE MEMBER_ID=1 // 회원과 관련된 주문
SELECT * FROM ORDERS WHERE MEMBER_ID=2 // 회원과 관련된 주문
SELECT * FROM ORDERS WHERE MEMBER_ID=3 // 회원과 관련된 주문
SELECT * FROM ORDERS WHERE MEMBER_ID=4 // 회원과 관련된 주문
SELECT * FROM ORDERS WHERE MEMBER_ID=5 // 회원과 관련된 주문

 

이처럼 처음 실행한 SQL의 결과 수만큼 추가로 SQL을 실행하는 것을 N + 1 문제라고 한다.

 

지연 로딩과 N + 1

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
    private List<Order> orders = new ArrayList<Order>();

    ...
}

지연 로딩으로 설정하면 JPQL에서는 N + 1 문제가 발생하지 않는다.

List<Member> members = 
    em.createQuery("select m from Member m", Member.class)
    .getResultList();

지연 로딩이므로 DB에서 회원만 조회된다.

따라서 다음 SQL만 실행되고 연관된 주문 컬렉션은 지연 로딩된다.

SELECT * FROM MEMBER

 

이후에 비즈니스 로직에서 주문 컬렉션을 실제 사용할 때 지연 로딩이 발생한다.

문제는 다음처럼 모든 회원에 대해 연관된 주문 컬렉션을 사용할 때 발생한다.

for(Member member : members)
{
    System.out.println("member = " + member.getOrders().size());
}

주문 컬렉션을 초기화하는 수만큼 다음 SQL이 실행될 수 있다.

회원이 5명이면 회원에 따른 주문도 5번 조회된다.

SELECT * FROM ORDERS WHERE MEMBER_ID=1 // 회원과 관련된 주문
SELECT * FROM ORDERS WHERE MEMBER_ID=2 // 회원과 관련된 주문
SELECT * FROM ORDERS WHERE MEMBER_ID=3 // 회원과 관련된 주문
SELECT * FROM ORDERS WHERE MEMBER_ID=4 // 회원과 관련된 주문
SELECT * FROM ORDERS WHERE MEMBER_ID=5 // 회원과 관련된 주문

결국 이것도 N + 1 문제다.

N + 1 문제는 즉시 로딩과 지연 로딩일 때 모두 발생할 수 있다.

 

해결 방법

 

1. 페치 조인 사용

N + 1 문제를 해결하는 가장 일반적인 방법은 페치 조인을 사용하는 것이다.

페치 조인은 SQL 조인을 사용해서 연관된 엔티티를 함꼐 조회하므로 N + 1 문제가 발생하지 않는다.

 

페치 조인 JPQL

select m from Member m join fetch m.orders

실행된 SQL

SELECT M.*, O.* FROM MEMBER M
INNER JOIN ORDERS O ON M.ID=O.MEMBER_ID

 

2. 하이버네이트 @BatchSize

하이버네이트가 제공하는 org.hibernate.annotations.BatchSize 어노테이션을 사용하면 연관된 엔티티를 조회할 때 지정한 size만큼 SQL의 IN절을 사용해서 조회한다.

만약 조회한 회원이 10명인데 size=5로 지정하면 2번의 SQL만 추가로 실행한다.

@Entity
public class Member
{
    ...
    @org.hibernate.annotations.BatchSize(size = 5)
    @OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
    private List<Order> orders = new ArrayList<Order>();
    ...
}

즉시 로딩으로 설정하면 조회 시점에 10건의 데이터를 모두 조회해야 하므로 다음 SQL이 두번 실행된다.

SELECT * FROM ORDERS
WHERE MEMBER_ID IN (
    ?, ?, ?, ?, ?
)

 

 

3. 하이버네이트 @Fetch(FetchMode.SUBSELECT)

하이버네이트가 제공하는 org.hibernate.annotations.Fetch 어노테이션에 FetchMode를 SUBSELECT로 사용하면 연관된 데이터를 조회할 때 서브 쿼리를 사용해서 N + 1 문제를 해결한다.

@Entity
public class Member
{
    ...
    @org.hibernate.annotations.Fetch(FetchMode.SUBSELECT)
    @OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
    private List<Order> orders = new ArrayList<Order>();
    ...
}

 

 

N+1 정리

즉시 로딩과 지연 로딩 중 추천하는 방법은 즉시 로딩은 사용하지 말고 지연 로딩만 사용하는 것이다.

즉시 로딩 전략은 그럴듯해 보이지만 N+1 문제는 물론이고 비즈니스 로직에 따라 필요하지 않은 엔티티를 로딩해야 하는 상황이 자주 발생한다.

그리고 즉시 로딩의 가장 큰 문제는 성능 최적화가 어렵다는 점이다.

엔티티를 조회하다보면 즉시 로딩이 연속으로 발생해서 전혀 예상하지 못한 SQL이 실행될 수 있다.

따라서 모두 지연 로딩으로 설정하고 성능 최적화가 꼭 필요한 곳에는 JPQL 페치 조인을 사용하자.

JPA의 글로벌 페치 전략 기본값은 다음과 같다:

  • @OneToOne, @ManyToOne: 기본 페치 전략은 즉시 로딩
  • @OneToMany, @ManyToMany: 기본 페치 전략은 지연 로딩

 

읽기 전용 쿼리의 성능 최적화

엔티티가 영속성 컨텍스트에 관리되면 1차 캐시부터 변경 감지까지 얻을 수 있는 혜택이 많다.

하지만 영속성 컨텍스트는 변경 감지를 위해 스냅샷 인스턴스를 보관하므로 더 많은 메모리를 사용하는 단점이 있다.

이때는  있다.

읽기 전용으로 엔티티를 조회하면 메모리 사용량을 최적화할 수 있다.

 

  • 스칼라 타입으로 조회
    • 가장 확실한 방법은 다음처럼 엔티티가 아닌 스칼라 타입을 모든 필드를 조회하는 것이다.
  • 읽기 전용 쿼리 힌트 사용
    • 하이버네이트 전용 힌트인 org.hibernate.readOnly 를 사용하면 엔티티를 읽기 전용으로 조회할 수 있다.
    • 읽기 전용이므로 영속성 컨텍스트는 스냅샷을 보관하지 않는다. 
    • 따라서 메모리 사용량을 최적화할 수 있다.
    • 단 스냅샷이 없으므로 엔티티를 수정해도 DB에 반영되지 않는다.
  • 읽기 전용 트랜젝션 사용
    • 스프링 프레임워크를 사용하면 트랜잭션을 읽기 전용 모드로 설정할 수 있다.
    • @Transactional(readonly = true)를 하면 강제로 플러시를 호출하지 않는 한 플러시가 일어나지 않는다.
  • 트랜잭션 밖에서 읽기
    • 트랜잭션 밖에서 읽는 다는 것은 트랜잭션 없이 엔티티를 조회하는 것이다.
    • JPA에선 수정 시 트랜잭션은 필수이기 때문에 조회가 목적일 때만 사용해야 한다.

배치 처리

수백만건의 데이터를 배치 처리해야 하는 상황이라 가정.

일반적인 방식으로 엔티티를 계속 조회하면 영속성 컨텍스트에 아주 많은 엔티티가 쌓이면서 메모리 부족 오류가 발생한다.

따라서 이런 배치 처리는 적절한 단위로 영속성 컨텍스트 초기화를 해야한다.

 

JPA 등록 배치

수천에서 수만 건 이상의 엔티티를 한 번에 등록할 때 주의할 점은 영속성 컨텍스트에 엔티티가 계속 쌓이지 않도록 일정 단위마다 영속성 컨텍스트의 엔티티를 DB에 플러시하고 영속성 컨텍스트를 초기화해야 한다.

만약 이런 작업을 하지 않으면 영속성 컨텍스트에 너무 많은 엔티티가 저장되면서 메모리 부족 오류가 발생할 수 있다.

EntityManager em = entityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();

for (int i = 0; i < 100000; i++) {
    Product product = new Product("item" + i, 10000);
    em.persist(product);

    // 100건마다 플러시와 영속성 컨텍스트 초기화
    if (i % 100 == 0) {
        em.flush();
        em.clear();
    }
}

tx.commit();
em.close();

 

 

JPA 페이징 배치 처리

EntityManager em = entityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction();

tx.begin();

int pageSize = 100;
for (int i = 0; i < 10; i++) {

    List<Product> resultList = em.createQuery("select p from Product p", Product.class)
        .setFirstResult(i * pageSize)
        .setMaxResults(pageSize)
        .getResultList();

    // 비즈니스 로직 실행
    for (Product product : resultList) {
        product.setPrice(product.getPrice() + 100);
    }

    em.flush();
    em.clear();
}

tx.commit();
em.close();

한번에 100건씩 페이징 쿼리로 조회하면서 상품의 가격을 100원 씩 증가한다.

그리고 페이지 단위마다 영속성 컨텍스트를 플러시하고 초기화한다.

 

 

하이버네이트 scroll사용

EntityTransaction tx = em.getTransaction();
Session session = em.unwrap(Session.class);
tx.begin();
ScrollableResults scroll = session.createQuery(
    "select p from Product p")
    .setCacheMode(CacheMode.IGNORE) // 2차 캐시 기능을 끈다.
    .scroll(ScrollMode.FORWARD_ONLY);

int count = 0;

while (scroll.next()) {
    Product p = (Product) scroll.get(0);
    p.setPrice(p.getPrice() + 100);

    count++;
    if (count % 100 == 0) {
        session.flush(); // 플러시
        session.clear(); // 영속성 컨텍스트 초기화
    }
}

tx.commit();
session.close();

scroll은 하이버네이트 전용 기능이므로 먼저 em.unwrap() 메서도를 사용해서 하이버네이트 세션을 구한다.

해서 하이버네이트 세션을 구한다.

다음으로 쿼리를 조회하면서 scroll() 메서드를 호출하면 엔티티 하나씩 조회할 수 있다.

 

하이버네이트 무상태 세션 사용

 

하이버네이트는 무상태 세션이라는 특별한 기능을 제공한다.

이름 그대로 무상태 세션은 영속성 컨텍스트를 만들지 않고 심지어 2차 캐시도 사용하지 않는다.

무상태 세션은 영속성 컨텍스트가 없다.

그리고 엔티티를 수정하려면 무상태 세션이 제공하는 update() 메서드를 직접 호출해야 한다.

SessionFactory sessionFactory = 
    entityManagerFactory.unwrap(SessionFactory.class);
StatelessSession session = sessionFactory.openStatelessSession();
Transaction tx = session.beginTransaction();
ScrollableResults scroll = session.createQuery(
    "select p from Product p").scroll();

while (scroll.next()) {
    Product p = (Product) scroll.get(0);
    p.setPrice(p.getPrice() + 100);
    session.update(p); // 직접 update를 호출해야 한다.
}

tx.commit();
session.close();

 

SQL 쿼리 힌트 사용

JPA는 DB SQL 힌트 기능을 제공하지 ㅇ낳는다.

SQL 힌트를 사용하려면 하이버네이트를 직접 사용해야 한다

SQL 힌트는 하이버네이트 쿼리가 제공하는 addQueryHint() 메서드를 사용한다.

Session session = em.unwrap(Session.class);

List<Member> list = session.createQuery("select m from Member m")
    .addQueryHint("FULL (MEMBER)") // SQL 힌트 추가
    .list();

실행된 SQL

select
    /*+ FULL (MEMBER) */ m.id, m.name
from
    Member m

현재 하이버네이트 버전에는 오라클 방언에만 힌트가 적용되어 있다.

다른 DB에서 SQL 힌트를 사용하려면 각 방언에서 org.hibernate.dialect.Dialect에 있는 다음 메서드를 오버라이딩해서 기능을 구현해야 한다.

 

트랜잭션을 지원하는 쓰기 지연과 성능 최적화

 

트랜잭션을 지원하는 쓰기 지연과 JDBC 배치

네트워크 호출 한 번은 단순한 메서드를 수만 번 호출하는 것보다 더 큰 비용이 든다.

JDBC가 제공하는 SQL 배치 기능을 사용하면 SQL을 모아서 DB에 한 번에 보낼 수 있다.

 

그래서 보통은 수백 수천 건 이상의 데이터를 변경하는 특수한 상황에 SQL 배치 기능을 사용한다.

JPA는 플러시 기능이 있으므로 SQL 배치 기능을 효과적으로 사용할 수 있다.

 

트랜잭션을 지원하는 쓰기 지연과 애플리케이션 확장성

트랜잭션을 지원하는 쓰기 지연과 변경 감지 기능 덕분에 성능과 개발의 편의성이라는 두 마리 토끼를 모두 잡을 수 있었다.

하지만 진짜 장점은 DB 테이블 로우에 락이 걸리는 시간을 최소화한다는 점이다.

이 기능은 트랜잭션을 커밋해서 영속성 컨텍스트를 플러시하기 전까지는 DB에 데이터를 등록, 수정, 삭제하지 않는다.

따라서 커밋 직전까지 DB 로우에 락을 걸지 않는다.

 

728x90

'JPA' 카테고리의 다른 글

JPA TIL - 09 값 타입  (0) 2024.05.15
JPA TIL - 07 고급 매핑  (0) 2024.05.08
JPA TIL - 06 다양한 연관관계 매핑  (0) 2024.04.16
JPA TIL - 05 연관관계 매핑 기초  (0) 2024.04.09
JPA TIL - 04 엔티티 매핑  (0) 2024.04.02
728x90

아래 노션에 정리해두었습니다!

https://www.notion.so/1-163b59d3ce2a46f8ba58e35bd65cce72?pvs=4

 

 

728x90
728x90

JPA의 데이터 타입을 가장 크게 분류하면 엔티티 타입과 값 타입으로 나눌 수 있다.

엔티티 타입은 @Entity로 정의하는 객체이고, 값 타입은 int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체를 말한다.

 

1. 값 타입

@Entity
public class Member
{
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    private int age;
    ...
}

위 코드 Member에서 String, int 가 값 타입이다.

Member 엔티티는 id라는 식별자 값도 가지고 생명주기도 있지만 값 타입인 name, age 속성은 식별자 값도 없고 생명주기도 회원 엔티티에 의존한다.

따라서 회원 엔티티 인스턴스를 제거하면 name, age 값도 제거된다. 그리고 값 타입은 공유하면 안된다.

예를 들어 다른 회원 엔티티의 이름을 변경한다고 해서 나의 이름까지 변경되는 것은 있어선 안된다.

 

2. 임베디드 타입(복합 값 타입)

새로운 값 타입을 직접 정의해서 사용할 수 있는데, JPA에서는 이것을 임베디드 타입이라고 한다.

직접 정의한 임베디드 타입도 int, String처럼 값 타입이라는 것이다.

@Entity
public class Member
{
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    // 근무 기간
    @Temporal(TemporalType.DATE) java.util.Date startDate;
    @Temporal(TemporalType.DATE) java.util.Date endDate;
    
    // 집 주소 표현
    private String city;
    private String street;
    private String zipcode;
    ...
}

위 코드는 평범한 회원 엔티티다.

 

"회원 엔티티는 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 가진다."

이런 설명은 단순히 정보를 나열한 것이다.

그리고 근무 시작일과 우편번호는 서로 아무 관련이 없다.

 

"회원 엔티티는 이름, 근무 기간, 집 주소를 가진다."

라는 표현이 더 명확하다.

 

회원이 상세한 데이터를 그대로 가지고 있는 것은 객체지향적이지 않으며 응집력만 떨어뜨린다.

대신에 근무 기간, 주소 같은 타입이 있다면 코드가 더 명확해질 것이다.

@Entity
public class Member
{
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    
    @Embedded Period workPeriod; // 근무 기간
    @Embedded Address homeAddress; // 집 주소
    ...
}

회원 엔티티가 더욱 의미 있고 응집력 있게 변했다.

@Embeddable
public class Period
{
    @Temporal(TemporalType.DATE) java.util.Date startDate;
    @Temporal(TemporalType.DATE) java.util.Date endDate;
    ..
    
    public boolean isWork(Date date)
    {
         ...
    }
}

startDate, endDate 를 합해서 Period 클래스를 만들었다.

@Embeddable
public class Address
{
    @Column(name = "city") // 매핑할 컬럼 정의 기능
    private String city;
    private String street;
    private String zipcode;
    ...
}

city, street, zipcode를 합해서 Address 클래스를 만들었다.

출처 : 자바 ORM 표준 JPA 프로그래밍

새로 정의한 값 타입들은 재사용할 수 있고 응집도도 높다.

또한 Period클래스의 isWork() 메서드처럼 해당 값 타입만 사용하는 의미 있는 메소드도 만들 수 있다.

 

임베디드 타입을 사용하려면 

@Embeddable : 값 타입을 정의하는 곳에 표시

@Embedded : 값 타입을 사용하는 곳에 표시

이 2가지 어노테이션이 필요하다.

 

또한 임베디드 타입은 기본 생성하자 필수이다.

 

더보기

01. 임베디드 타입과 테이블 매핑

출처 : 자바 ORM 표준 JPA 프로그래밍

임베디드 타입은 엔티티의 값일 뿐이다.

따라서 값이 속한 엔티티의 테이블에 매핑한다.

 

임베디드 타입 덕분에 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능하다.

잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.

 

테이블 하나에 여러 클래스를 매핑하는 것은 상상하기도 싫을 것이다..

이런 지루한 반복 작업은 JPA에게 맡기고 더 세밀한 객체지향 모델을 설계하는 데에 집중해야한다.

 

02. 임베디드 타입과 연관관계

임베디드 타입은 값 타입을 포함하거나 엔티티를 참조할 수 있다.

출처 : 자바 ORM 표준 JPA 프로그래밍
@Entity
public class Member
{
    @Embedded Address address; // 임베디드 타입 포함
    @Embedded PhoneNumber phoneNumber; // 임베디드 타입 포함
    ...
}

@Embeddable
public class Address
{
    String street;
    String city;
    String state;
    @Embedded Zipcode zipcode; // 임베디드 타입 포함
}

@Embeddable
public class Zipcode
{
    String zip;
    String plusFour;
}

@Embeddable
public class PhoneNumber
{
    String areaCode;
    String localNumber;
    @ManyToOne PhoneServiceProvider provider; // 엔티티 참조
    ...
}

@Entity
public class PhoneServiceProvider
{
    @Id
    String name;
    ...
}

값 타입인 Address 가 값 타입인 Zipcode를 포함하고, 값 타입인 PhoneNumber가 엔티티 타입인 PhoneServiceProvider를 참조한다.

 

03. @AttrivuteOverride : 속성 재정의

임베디드 타입에 정의한 매핑정보를 재정의하려면 엔티티에 @AttributeOverride를 사용하면 된다.

@Entity
public class Member
{
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    
    @Embedded Address homeAddress;
    @Embedded Address companyAddress;
}

만약 회원에게 주소가 하나 더 필요하면 어떻게 해야할까?

위 코드에서 문제는 테이블에 매핑하는 컬럼 명이 중복되는 것이다.

이때는 @AttributeOverrides를 사용해서 매핑정보를 재정의해야 한다.

 

@Entity
public class Member
{
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    
    @Embedded Address homeAddress;
    
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "city", column = @Column(name = "COMPANY_CITY")),
        @AttributeOverride(name = "street", column = @Column(name = "COMPANY_STREET")),
        @AttributeOverride(name = "zipcode", column = @Column(name = "COMPANY_ZIPCODE"))
    }
    Address companyAddress;
}

이렇게 엔티티를 정의하면

CREATE TABLE MEMBER
(
    COMPANY_CITY varchar(255),
    COMPANY_STREET varchar(255),    
    COMPANY_ZIPCODE varchar(255),    
    city varchar(255),
    street varchar(255),
    zipcode varchar(255),
    ...
)

이렇게 생성된다.

 

04. 임베디드 타입과 null

임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이 된다.

 

3. 값 타입과 불변 객체

값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다.

따라서 값 타입은 단순하고 안전하게 다룰 수 있어야 한다.

 

더보기

01. 값 타입 공유 참조

임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.

출처 : 자바 ORM 표준 JPA 프로그래밍

위 그림의 상황은

memver1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();

address.setCity("NewCity"); // 회원1의 address 값을 공유해서 사용
member2.setHomeAddress(address);

이렇게 표현할 수 있다.

 

회원2에 새로운 주소를 할당하려고 회원1의 주소를 그대로 참조해서 사용했다.

이 코드를 실행하면 회원1의 주소도 "NewCity"로 변경된다.

영속성 컨텍스트에서는 회원1과 회원2 둘 다 city 속성이 변경된 것으로 판단해서 회원1, 회원2 각각 UPDATE SQL을 실행한다.

이런 부작용을 막으려면 값을 복사해서 사용하면 된다.

 

02. 값 타입 복사

값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하다.

대신에 값(인스턴스)을 복사해서 사용해야 한다.

출처 : 자바 ORM 표준 JPA 프로그래밍

위 그림의 상황은

member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();

// 회원1의 address 값을 복사해서 새로운 newAddress 값을 생성
Address newAddress = address.clone();

newAddress.setCity("NewCity");
member2.setHomeAddress(newAddress);

이렇게 표현할 수 있다.

 

회원2에 새로운 주소를 할당하기 위해 clone() 메소드를 만들었는데, 이 메소드는 자신을 복사해서 반환하도록 구현했다.

그러면 영속성 컨텍스트는 회원2의 주소만 변경된 것으로 판단해서 회원2에 대해서만 UPDATE SQL을 실행한다.

 

이처럼 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.

문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이라는 것이다.

 

자바는 기본 타입에 값을 대입하면 값을 복사해서 전달한다.

 

int a = 10;
int b = a; // 기본 타입은 항상 값을 복사한다.
b = 4;

이 코드의 최종 결과는 a = 10, b = 4 다.

int b = a에서 a의 값 10을 복사해서 b에 넘겨준다.

따라서 a, b는 완전히 독립된 값을 가지고 부작용도 없다.

 

문제는 Address같은 객체 타입이다.

자바는 객체에 값을 대입하면 항상 참조값을 전달한다.

Address a = new Address("Old");
Address b = a; // 객체 타입은 항상 참조 값을 전달한다.
b.setCity("New");

Address b = a 에서 a가 참조하는 인스턴스의 참조 값을 전달하기 때문에 

b에서 city를 바꿔도 a의 city도 같이 바뀌게 된다.

 

Address a = new Address("Old");
Address b = a.clone(); // 항상 복사해서 넘겨야 한다.
// Address b = a; // 이렇게 참조만 넘기면 부작용이 발생할 수 있다.
b.setCity("New");

이때도 값 타입 복사를 해주면 된다.

 

객체의 공유 참조는 피할 수 없다.

따라서 근본적인 해결책이 필요한데 가장 단순한 방법은 객체의 값을 수정하지 못하게 막으면 된다.

 

03. 불변 객체

값 타입은 부작용 걱정 없이 사용할 수 있어야 한다.

부작용이 일어나면 값 타입이라 할 수 없다.

객체를 불변하게 만들면 값을 수정할 수 없으므로 부작용을 원천 차단할 수 있다.

따라서 값 타입은 될 수 있으면 불변 객체로 설계해야 한다.

@Embeddable
public class Address
{
    private String city;
    
    protected Address() {} // JPA에서 기본 생성자는 필수다.
    
    // 생성자로 초기 값을 설정한다.
    public Address(String city) {this.city = city}
    
    // 접근자(Getter)는 노출한다.
    public String getCity()
    {
        return city;
    }
    
    // 수정자(Setter)는 만들지 않는다.
}

위의 Address 는 이제 불변 객체이다.

Address address = member1.getHomeAddress();
// 회원1의 주소 값을 조회해서 새로운 주소값을 생성
Address newAddress = new Address(address.getCity());
member2.setHomeAddress(newAddress);

만약 값을 수정해야 하면 위 코드처럼 새로운 객체를 생성해서 사용해야 한다.

 

4. 값 타입의 비교

int a = 10;
int b = 10;

Address a = new Address("서울시", "종로구", "1번지");
Address b = new Address("서울시", "종로구", "1번지");

int a의 숫자 10과 int b의 숫자 10은 같다고 표현한다.

Address a 와 Address b는 같다고 표현한다.

 

자바가 제공하는 객체 비교는 2가지가 있다.

  • 동일성 비교 : 인스턴스 참조 값을 비교, == 사용
  • 동등성 비교 : 인스턴스의 값을 비교, equals()사용

Address값 타입을 a == b로 동일성 비교하면 둘은 서로 다른 인스턴스이므로 결과는 거짓이다.

값 타입은 비록 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야한다.

따라서 값 타입을 비교할 때는 a.equals(b)를 사용해서 동등성 비교를 해야한다.

equals()메소드는 재정의 해야한다.

 

5. 값 타입 컬렉션

값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollection, @CollectionTable 어노테이션을 사용하면 된다.

@Entity
public class Member
{
    @Id
    @GeneratedValue
    private Long id;
    
    @Embedded
    private Address HomeAddress;
    
    @ElementCollection
    @CollectionTable(name = "FAVORITE_FOODS",
        joinColumns = @JoinColumn(name = "MEMBER_ID"))
    @Column(name="FOOD_NAME")
    private Set<String> favoriteFoods = new HashSet<String>();
    
    @ElementCollection
    @CollectionTable(name = "ADDRESS", joinColumns
        = @JoinColumn(name = "MEMBER_ID"))
    private List<Address> addressHistory = new ArrayList<Address>();
    ...
}

Member 엔티티를 보면  값 타입 컬렉션을 사용하는 favoriteFoods, addressHistory에 @ElementCollection을 지정했다.

 

favoriteFoods는 기본 값 타입인 String을 컬렉션으로 가진다.

이것을 DB 테이블로 매핑해야 하는데 관계형 DB의 테이블을 컬럼안에 컬렉션을 포함할 수 없다.

출처 : 자바 ORM 표준 JPA 프로그래밍
출처 : 자바 ORM 표준 JPA 프로그래밍

따라서 위 그림처럼 별도의 테이블을 매핑해야 한다.

그리고 favoriteFoods처럼 값으로 사용되는 컬럼이 하나면 @Column을 사용해서 컬럼명을 지정할 수 있다.

 

addressHistory는 임베디드 타입인 Address를 컬렉션으로 가진다.

이것도 마찬가지로 별도의 테이블을 사용해야 한다.

테이블 매핑정보는 @AttributeOverride를 사용해서 재정의할 수 있다.

 

더보기

01. 값 타입 컬렉션 사용

Member member = new Member();

// 임베디드 값 타입
member.setHomeAddress(new Address("통영", "몽돌해수욕장", "660-123"));

// 기본값 타입 컬렉션
member.getFavoriteFoods().add("짬뽕");
member.getFavoriteFoods().add("짜장");
member.getFavoriteFoods().add("탕수육");

// 임베디드 값 타입 컬렉션
member.getAddressHistory().add(new Address("서울", "강남", "123-123"));
member.getAddressHistory().add(new Address("서울", "강북", "000-000"));

em.persist(member);

위 코드는 등록하는 코드다.

마지막에 member엔티티만 영속화 했다.

JPA는 이때 member의 엔티티 값 타입도 함께 저장한다.

  • member : INSERT SQL 1번
  • member.homeAddress : 컬렉션이 아닌 임베디드 값 타입이므로 회원테이블을 저장하는 SQL에 포함된다.
  • member.favoriteFoods : INSERT SQL 3번
  • member.addressHistory : INSERT SQL 2번

따라서 em.persist(member) 한 번 호출로 총 6번의 INSERT SQL을 실행한다.

 

 

// SQL : SELECT ID, CITY, STREET, ZIPCODE FROM MEMBER WHERE ID = 1
Member member = em.find(Member.class, 1L); // 1. member

// 2. member.homeAddress
Address homeAddress = member.getHomeAddress();

// 3. member.favoriteFoods
Set<String> favoriteFoods = member.getFavoriteFoods(); // LAZY

// SQL : SELECT MEMBER_ID, FOOD_NAME FROM FAVORITE_FOODS WHERE MEMBER_ID = 1
for (String favoriteFood : favoriteFoods)
{
    System.out.println("favoriteFood = " + favoriteFood);
}

// 4. member.addressHistory
List<Address> addressHistory = member.getAddressHistory(); // LAZY

// SQL : SELECT MEMBER_ID , CITY, STREET, ZIPCODE FROM ADDRESS WHERE MEMBER_ID = 1
addressHistory.get(0);

지연로딩으로 설정

 

  1. member : 회원만 조회한다. 이때 임베디드 값 타입인 homeAddress도 함께 조회한다. SELET SQL을 1번 호출한다.
  2. member.homeAddress : 1번에서 회원을 조회할 때 같이 조회해둔다.
  3. member.favoriteFoods : LAZY로 설정해서 실제 컬렉션을 사용할 때 SELECT SQL을 1번 호출한다.
  4. member.addressHistory : LAZY로 설정해서 실제 컬렉션을 사용할 때 SELECT SQL을 1번 호출한다.
Member member = em.find(Member.class, 1L);

// 1. 임베디드 값 타입 수정
member.sethomeAddress(new Address("새로운도시", "신도시1", "123456"));

// 2. 기본값 타입 컬렉션 수정
Set<String> favoriteFoods = member.getFavoriteFoods();
favoriteFoods.remove("탕수육");
favoriteFoods.add("치킨");

// 3. 임베디드 값 타입 컬렉션 수정
List<Address> addressHistory = member.getAddressHistory();
addressHistory.remove(new Address("서울", "기존 주소", "123-123"));
addressHistory.add(new Address("새로운도시", "새로운 주소", "123-456"));
  • 임베디드값 타입 수정 : homeAddress 임베디드 값 타입은 MEMBER 테이블과 매핑했으므로 MEMBER 테이블만 UPDATE한다. 사실 Member 엔티티를 수정하는 것과 같다.
  • 기본값 타입 컬렉션 수정 : 탕수육을 치킨으로 변경하려면 탕수육을 제거하고 치킨을 추가해야 한다. 자바의 String타입을 수정 불가하다.
  • 임베디드 값 타입 컬렉션 수정 : 값 타입은 불변해야 한다. 따라서 컬렉션에서 기존 주소를 삭제하고 새로운 주소를 등록했다. 참고로 값 타입은 equals, hashcode를 꼭 구현해야 한다.

 

02. 값 타입 컬렉션 제약사항

엔티티는 식별자가 있어서 엔티티 값을 변경해도 식별자로 DB에 젖아된 원본 데이터를 쉽게 찾아서 변경 가능하다.

하지만 값 타입은 식별자가 없고 단순한 값들의 모음이므로 값을 변경해버리면 DB에 저장된 원본 데이터를 찾기 어렵다.

 

특정 엔티티 하나에 소속된 값 타입은 값이 변경되어도 자신의 소속된 엔티티를 DB에서 찾고 값을 변경하면 된다.

문제는 값 타입 컬렉션이다. 

값 타입 컬렉션에 보관된 값 타입들은 별도의 테이블에 보관되어서 DB에 있는 원본 데이터를 찾기 어렵다.

 

이런 문제로 인해 JPA 구현체들은 값 타입 컬렉션에 변경사항이 발생하면 값테입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고, 현제 값 타입 컬렉션 객체제 있는 모든 값을 DB에 다시 저장한다.

 

따라서 실무에서는 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신에 일대다 관계를 고려해야 한다.

 

지금까지 나온 문제를 해결하려면 값 타입 컬렉션을 사용하는 대신에 새로운 엔티티를 만들어서 일대다 관계로 설정하면 된다.

여기에 추가로 영속성 전이(Cascade) + 고아 객체 제거(ORPHAN REMOVE) 기능을 적용하면 값 타입 컬렉션처럼 사용할 수 있다.

728x90

'JPA' 카테고리의 다른 글

JPA TIL - 15 고급 주제와 성능 최적화  (0) 2024.07.10
JPA TIL - 07 고급 매핑  (0) 2024.05.08
JPA TIL - 06 다양한 연관관계 매핑  (0) 2024.04.16
JPA TIL - 05 연관관계 매핑 기초  (0) 2024.04.09
JPA TIL - 04 엔티티 매핑  (0) 2024.04.02
728x90

메모리 관리 기법중 하나로 프로세스 전체가 메모리 내에 올라오지 않더라도 실행이 가능하도록 하는 기법

 

1. 사용자 프로그램이 물리 메모리의 제약에서 벗어남

2. 각 프로그램이 더 작은 메모리를 차지하기 때문에 더 많은 프로그램을 동시수행 가능

3. 프로그램을 메모리에 올리고 swap하는데 필요한 IO횟수가 줄어듦

 

 

가상 메모리 관리 : 지금 당장 실행해야 하는 부분만 메인 메모리에 저장하고 나머지 프로그램은 보조기억장치에 둔 채 동작하는 방법

실행할 프로그램 크기가 메인 메모리보다 크거나 개수가 많으면 메인메모리 내 공간이 부족해 프로그램이 제대로 실행되지 못하는 문제를 해결하기 위해 개발

 

페이징 기법 : 프로그램을 일정 크기로 나누어 페이지를 만들고 페이지 단위별로 메인메모리에 올려 동작하는 방식

 

페이지 테이블에서 몇번째 페이지가 물리 주소의 어느 주소 번지에 있는지 명시되어있다.

만약 32비트 머신이 있다고 치자.

그럼 프로세스가 접근할 수 있는 메모리 주소의 범위는 각 주소가 32개의 비트로 구성되어 있을 것이다.

32비트, 즉 2의 32승개의 고유한 주소를 나타낼 수 있다.

따라서 32비트 머신의 주소 공간은 최대 4GB이다.

 

페이지 폴트(Page Fault)란?

컴퓨터 시스템에서 프로세스가 요청한 메모리 페이지가 현재 물리적 메모리(RAM)에 존재하지 않을 때 발생하는 이벤트이다.

 

페이지 폴트 발생 과정

1. 프로세스가 페이지 접근 시도 : 프로세스가 특정 데이터에 접근 시도 시, 해당 데이터가 저장된 메모리 페이지에 접근

2. 페이지 테이블 조회 : CPU가 페이지 테이블을 조회하여 해당 가상 페이지가 물리적 메모리에 매핑되어 있는지 확인

3. 페이지 부재 확인 : 페이지 폴트 발생

 

페이지 폴트 처리 과정

1. 페이지 폴트 핸들러 호출 : 운영체제의 페이지 폴트 핸들러 호출

2. 디스크에서 페이지 로드 : 필요한 페이지를 디스크에서 물리적 메모리로 로드

3. 페이지 테이블 업데이트 : 페이지 테이블을 업데이트하여 새로 로드된 페이지의 물리적 메모리 주소 기록

4. 프로세스 재개 : 필요한 페이지가 메모리에 로드되면 프로세스는 중단된 지점에서 다시 시작

 

페이지 트레이드 오프(Trade-Off)란?

주로 가상 메모리 시스템에서 메모리 관리 전략과 성능 간의 균형을 맞추기 위해 고려되는 다양한 선택.

페이지 시스템에서는 메모리 사용의 효율성과 시스템 성능을 최적화 하려고 하는데, 이 과정에서 여러 가지 트레이드 오프가 발생

 

1. 페이지 크기

큰 페이지와 작은 페이지로 나뉜다.

2. 페이지 교체 알고리즘

최적 교체 알고리즘과 LRU(최근에 사용되지 않은 페이지 교체)와 FIFO알고리즘 사용

3. 페이지 파일 크기와 위치

큰 페이지 파일은 더 많은 가상 메모리, 작은 페이지 파일은 더 적은 가상 메모리

4. 메모리 할당 전략

최초 적합과 최적 적합

5. 프리페이징 수요페이징

초기에 로드 할 때 페이지 폴트를 줄일 수 있음, 실제로 필요한 페이지만 로드하여 메모리를 효율적으로 사용 가능

6. 쓰기 전략

즉시 쓰기, 지연 쓰기

 

페이지 크기가 커지면, 페이지 폴트가 더 많이 발생할까?

보통은 페이지 폴트가 감소한다. 

페이지가 크면 반대로 페이지 테이블 크기는 감소한다. 때문에 테이블 탐색하는 시간이 줄어든다.

한번의 페이지 폴트로 더 많은 데이터를 메모리에 로드할 수 있기 때문에 결과적으로 페이지 폴트의 빈도를 줄인다.

 

세그멘테이션 방식을 사용하고 있다면 가상 메모리를 사용할수 없을까?

세그멘테이션 방식

프로그램을 논리적 block으로 분할

block의 크기가 서로 다를 수 있음

때문에 메모리를 미리 분할할 수 없음

세그먼트를 공유, 보호하기 용이함

주소 매핑 및 메모리 관리의 오버헤드가 크다.

내부 단편화가 발생하지 않음(크기를 조절할 수 있기 때문, 외부 단편화는 생길 수 있음)

 

주소 매핑

가상 주소 : v = (s, d)

s는 세그먼트 번호, d는 오프셋

세그먼트는 논리적으로 분할되어 있어서 공유 및 보호가 용이하다.

페이징에서는 공유할 때 주소를 뛰어넘어서 공유를 해야했는데, 세그먼트는 논리적인 공간에서 공유가 용이해진다.

 

 

결국 세그멘테이션 방식도 가상 메모리 시스템의 한 형태이다.

 

 

페이지와 프레임의 차이

페이지

가상 메모리의 고정된 크기의 블록, 프로세스의 가상 주소 공간을 나누는 단위

프레임

물리적 메모리의 고정된 크기의 블록, 물리 메모리를 나누는 단위로 가상 메모리의 페이지가 매핑되는 대상

 

내부 단편화와 외부 단편화란?

내부 단편화

할당 된 메모리 블록 내에서 실제로 사용되지 않는 여유 공간이 생기는 현상

외부 단편화

메모리의 여러 작은 비어있는 블록들로 인해 전체적으로 충분한 메모리가 있음에도 불구하고, 큰 연속적인 메모리 블록을 할당할 수 없는 상태

 

페이지에서 실제 주소를 어떻게 가져올 수 있나?

 

어떤 주소 공간이 있을 때, 수정 가능한지 확인할 수 있는 방법

운영 체제에서의 메모리 보호기능

1. 페이지 권한 설정

2. 메모리 보호 오류 처리

3. 메모리 보호 관련 시스템 호출

4. 주소 공간 분할 (예 : 코드 영역은 실행만 가능하고 읽기만 가능한 권한, 데이터 영역은 읽기/쓰기 가능한 권한)

 

32비트에서 페이지 크기가 1KB라면 페이지 테이블의 최대 크기는?

페이지의 크기가 1KB인 경우, 1KB 페이지는 2^10바이트이다.

32비트 크기는 4GB(2^32)이다.

즉 페이지 테이블의 최대 크기는 2^32 / 2^10 인 2^22이다.

 

32비트 운영체제는 램을 최대 4G까지 사용가능하다. 그 이유는?

32비트 운영체제에서는 가상 주소 공간의 크기가 최대 4GB이므로 운영체제는 이를 기준으로 최대로 사용할 수 있는 램의 용량을 결정한다.

즉, 운영체제는 최대 4GB의 램을 사용하도록 설정된다.

 

C/C++ 개발을 하게 되면 Segmentation Fault라는 에러를 접할 수 있을 텐데, 이 에러는 세그멘테이션/페이징과 어떤관계가 있나?

세그멘테이션 오류는 프로그램이 잘못된 메모리 영역에 접근하여 발생하는 오류이다.

주로 배열 범위를 초과하거나 NULL포인터를 참조하는 등의 경우에 발생 가능하다.

세그멘테이션 오류가 발생할 때, 이는 가상 주소를 올바르게 페이징하지 못하거나 잘못된 물리 주소로 매핑될 수 있음을 의미한다.

 

 

Thrashing이란?

페이지 부재율(Page fault)이 증가하여 CPU 이용율이 급격하게 떨어지는 현상이다.

 

해결방법은?

Working set

지역성의 원리를 이용하여 지역성 집합이 메모리에 동시에 올라갈 수 있도록 보장하는 메모리 관리 방법이다.

 

Page Fault Frequency(페이지 부재 빈도)

프로세스의 페이지 부재율을 주기적으로 조사하고 이 값에 근거하여 각 프로세스에 할당할 메모리 양을 동적으로 예측하고 조절하는 알고리즘이다.

728x90

'기술 면접 > 운영체제' 카테고리의 다른 글

프로세스란?  (0) 2024.05.09
인터럽트란?  (0) 2024.05.09
시스템 콜이란?  (0) 2024.05.09
728x90

컴퓨터에서 실행중인 프로그램을 의미한다.

프로그램은 하드디스크 등의 저장공간에 저장되어 있는 코드와 리소스 등이 집합되어 있는 파일을 말하고 실행하면 메모리에 올라가서 프로세스로 동작한다.

다시 말해서 실행되는 프로그램들은 전부 프로세스라고 보면 된다.

 

1. 프로세스, 스레드의 차이

  1. 정의
    • 프로세스: 운영 체제에서 실행되는 프로그램의 인스턴스로, 각 프로세스는 독립적인 메모리 공간을 가지고 실행된다.
    • 프로세스는 자신만의 주소 공간, 파일 디스크립터, 시스템 자원, 스레드 등을 가진다.
    • 스레드: 프로세스 내에서 실행되는 작업의 단위로, 하나의 프로세스 내에서 여러 개의 스레드가 동시에 실행될 수 있다.
    • 스레드는 프로세스의 자원을 공유하며, 동일한 주소 공간에서 실행된.
  2. 자원 공유
    • 프로세스: 각 프로세스는 독립적인 메모리 공간을 가지고 있으므로, 프로세스 간에 자원을 공유하기 위해서는 명시적인 IPC(Inter-Process Communication) 메커니즘이 필요합니다.
    • 스레드: 같은 프로세스 내의 스레드는 메모리 공간을 공유하므로, 전역 변수나 힙 메모리 등의 자원에 대한 접근이 쉽습니다.

 

2. PCB란?

PCB는 운영체제가 프로세스를 제어하기 위해 정보를 저장해 놓는 곳으로, 프로세스의 상태 정보를 저장하는 자료구조이다.

프로세스가 생성될 때마다 고유의 PCB가 생성되고, 프로세스가 완료되면 PCB도 함께 제거된다.

  1. 프로세스 식별자(Process ID)
    • PCB에는 각 프로세스를 식별하는 고유한 식별자가 포함된다.
    • 이 식별자는 프로세스의 생성 순서나 다른 방식으로 할당된다.
  2. 프로세스 상태(Process State)
    • 프로세스의 현재 상태를 나타내는 정보이다.
    • 대기(waiting), 실행(running), 준비(ready), 종료(terminated) 등의 상태가 포함될 수 있다.
  3. 프로그램 카운터(Program Counter, PC)
    • 현재 실행 중인 프로세스의 다음 실행할 명령어의 주소를 나타내는 레지스터 값이다.
  4. 레지스터 상태(Register State)
    • 프로세스가 사용하는 CPU 레지스터의 상태를 저장한다.
    • 이는 프로세스가 다시 실행될 때 이전 상태로 복원할 수 있도록 도와준다.
  5. 스케줄링 정보(Scheduling Information)
    • 프로세스의 우선순위, 할당된 CPU 시간, 스케줄링 알고리즘과 관련된 정보 등이 포함된다.

 

 

3. 스레드 PCB

프로세스의 PCB와는 조금 다른 용도와 구조를 갖고 있다.

스레드의 PCB에는 해당 스레드에 대한 정보가 포함되어 있다.

  1. 스레드 식별자(Thread ID)
    • 각 스레드를 식별하는 고유한 식별자가 포함된다.
  2. 스레드 상태(Thread State)
    • 스레드의 현재 상태를 나타내는 정보로, 실행(running), 대기(waiting), 준비(ready) 등의 상태가 포함될 수 있다.
  3. 프로그램 카운터(Program Counter, PC)
    • 현재 실행 중인 스레드의 다음 실행할 명령어의 주소를 나타내는 레지스터 값이다.
  4. 스레드 레지스터 상태(Thread Register State)
    • 스레드가 사용하는 CPU 레지스터의 상태를 저장합니다. 이는 스레드가 다시 실행될 때 이전 상태로 복원할 수 있도록 도와준다.
  5. 스케줄링 정보(Scheduling Information)
    • 스레드의 우선순위, 할당된 CPU 시간 등 스케줄링과 관련된 정보가 포함된다.

 

4. 리눅스에서 프로세스/스레드 생성

  1. 프로세스 생성
    • 새로운 프로세스를 생성하기 위해서는 fork() 시스템 콜을 사용한다.
    • 이 시스템 콜은 현재 실행 중인 프로세스를 복제하여 새로운 프로세스를 생성한다.
    • 새로운 프로세스는 부모 프로세스의 상태를 상속받으며, 자신의 고유한 프로세스 ID(PID)를 갖는다.
    • 그 후, 새로운 프로세스는 exec() 시스템 콜을 사용하여 다른 프로그램을 실행할 수 있다.
  2. 스레드 생성
    • 리눅스에서는 POSIX 스레드(pthread)를 지원하며, POSIX 스레드는 pthread_create() 함수를 사용하여 생성된다.
    • 이 함수는 새로운 스레드를 생성하고 실행할 함수와 인수를 지정한다.
    • 새로운 스레드는 실행할 함수의 시작 지점에서부터 실행한다.
    • 새로운 스레드는 같은 프로세스 내의 다른 스레드와 메모리 공간을 공유하므로, 주의하여 동기화를 해야 한다.

 

5. 자식 프로세스가 상태를 알리지 않고 죽거나, 부모 프로세스가 먼저 죽게 될 때 처리법

  1. 자식 프로세스가 비정상적으로 종료된 경우
    • 자식 프로세스가 비정상적으로 종료되면, 리눅스에서는 부모 프로세스가 SIGCHLD 시그널을 받는다.
    • 이 시그널을 처리하는 일반적인 방법은 wait() 또는 waitpid() 시스템 콜을 사용하여 자식 프로세스의 종료 상태를 확인하는 것이다.
    • 이를 통해 자식 프로세스가 종료되었고, 종료 상태를 읽어들여 자원을 회수하고 관련 정보를 처리할 수 있다.
  2. 부모 프로세스가 먼저 종료된 경우
    • 부모 프로세스가 종료되면, 자식 프로세스는 고아 프로세스(Orphan Process)가 된다.
    • 리눅스에서는 고아 프로세스를 관리하기 위해 특별한 처리를 한다.
    • 부모 프로세스가 종료되면, 해당 자식 프로세스의 부모 프로세스가 init 프로세스가 되어 이를 책임지게 된다.
    • init 프로세스는 시스템의 초기 프로세스로, 고아 프로세스를 수집하여 정리하는 역할을 한다.
    • 따라서 자식 프로세스가 고아 프로세스가 되더라도 시스템에서 자동으로 처리된다.

 

6. 리눅스 데몬프로세스

리눅스 데몬 프로세스(Daemon Process)는 백그라운드에서 실행되는 프로세스로, 사용자와 상호작용 없이 시스템 서비스나 특정 작업을 수행한다.

데몬은 보통 부팅 시 자동으로 시작되어 시스템이 실행 중일 때 지속적으로 실행된다.

 

  1. 백그라운드 실행
    • 데몬 프로세스는 보통 백그라운드(background)에서 실행된다.
    • 즉, 사용자와의 상호작용이 없이 동작합니다.
  2. 부팅 시 자동 시작
    • 시스템이 부팅될 때 자동으로 시작되어야 하는 서비스나 프로세스로 사용된다.
    • 이를 위해 보통 초기화 스크립트(init scripts) 또는 systemd 단위 파일(systemd unit files)을 사용하여 설정된다.
  3. 세션 독립성
    • 데몬은 사용자 세션과 독립적으로 실행된다.
    • 사용자가 로그아웃하거나 종료해도 데몬은 계속해서 백그라운드에서 실행된다.
  4. 시스템 서비스 제공
    • 주로 시스템 서비스나 백그라운드 작업을 처리한다.
    • 예를 들어, 웹 서버(Apache), 데이터베이스 서버(MySQL), 로그 관리(daemonlogger) 등이 데몬 프로세스의 예시이다.
  5. 프로세스 관리 및 모니터링
    • 데몬은 프로세스 관리를 위해 시스템 리소스와 프로세스 상태를 모니터링하고, 필요에 따라 로깅 및 알림을 수행할 수 있다.

 

7. Init 프로세스

리눅스에서는 모든 프로세스가 프로세스 트리(process tree)를 형성한다.

이 트리의 루트 노드에 해당하는 프로세스는 init 프로세스이다.

init 프로세스는 시스템 부팅 시 가장 먼저 실행되는 프로세스이며, 모든 다른 프로세스는 init 프로세스의 자식이거나 자식의 자식으로서 프로세스 트리를 형성한다.

 

  1. 부팅 및 초기화
    • 시스템 부팅 시 필요한 서비스와 프로세스를 시작하고 초기화 한다.
  2. 서비스 관리
    • 백그라운드에서 실행되어야 하는 서비스들을 관리하고, 필요에 따라 시작하거나 중지한다.
  3. 시스템 리소스 초기화
    • 시스템 리소스를 초기화하고, 필요한 설정을 로드한다.
  4. 문제 해결 및 로깅
    • 부팅 및 서비스 시작 중 발생하는 문제를 감지하고, 로깅하여 시스템 관리자가 문제를 해결할 수 있도록 한다.
728x90

'기술 면접 > 운영체제' 카테고리의 다른 글

가상메모리란?  (0) 2024.05.15
인터럽트란?  (0) 2024.05.09
시스템 콜이란?  (0) 2024.05.09
728x90

인터럽트란 CPU가 특정 기능을 수행하는 도중에 급하게 다른 일을 처리하고자 할 때 사용할 수 있는 기능이다.

 

대부분 컴퓨터는 한개의 CPU를 사용하므로 "한 순간"에는 하나의 일 밖에 처리할 수 없다.

때문에 우선순위가 더 높은 일을 처리할 필요가 있을 때 대처할 수 있는 방안이 필요하다.

그것이 인터럽트이다.

 

1. 인터럽트 처리 방식

  1. 인터럽트 발생
    • 하드웨어나 소프트웨어에서 인터럽트가 발생한다.
    • 예를 들어, 하드웨어 장치에서 데이터 전송 완료, 타이머 만료, 키보드 입력 등과 같은 이벤트가 발생할 수 있다.
  2. 현재 작업 중단
    • 프로세서는 현재 수행 중인 작업을 일시 중단하고, 실행 중인 프로그램의 상태를 저장한다.
    • 이는 중단된 작업을 나중에 다시 시작할 수 있도록 있게 다.
  3. 인터럽트 서비스 루틴 실행
    • 인터럽트 발생 시, 해당 인터럽트에 대한 처리를 담당하는 인터럽트 서비스 루틴(Interrupt Service Routine, ISR)이 실행됩니다.
    • 이 루틴은 인터럽트를 발생시킨 장치 또는 소프트웨어 이벤트에 따라 다르게 작동한다.
  4. 인터럽트 처리
    • ISR은 해당 인터럽트에 대한 필요한 작업을 수행한다.
    • 이는 주로 인터럽트 발생 이유에 따라 다르며, 예를 들어 장치에서 데이터를 읽어오거나, 시스템의 상태를 업데이트하거나, 특정 작업을 수행하는 등 다양한 작업을 포함할 수 있습니다.
  5. 현재 작업 재개
    • 인터럽트 서비스 루틴이 실행을 마치면, 프로세서는 원래 실행 중인 프로세스나 작업으로 복귀한다.
    • 이전에 저장한 프로그램 상태를 복원하여 중단된 작업을 계속한다.

 

2. Polling 방식

폴링(Polling)은 컴퓨터 시스템에서 외부 이벤트를 감지하기 위한 기술 중 하나로, 주기적으로 상태를 확인하여 이벤트가 발생했는지 여부를 확인하는 방법이다.

폴링은 주로 입출력 장치와의 상호 작용에서 사용된다.

  1. 장치 상태 확인
    • 폴링을 사용하는 시스템은 주기적으로 특정 장치의 상태를 확인한다
    • 이는 주로 특정 레지스터 값을 읽거나, 특정 비트를 확인하여 수행한다.
  2. 상태 확인 및 이벤트 처리
    • 장치의 상태가 변경되었는지 여부를 확인한다.
    • 상태가 변경되었다면, 해당 이벤트에 대한 처리를 수행한다.
    • 예를 들어, 장치에서 데이터가 도착했다는 것을 감지하고, 데이터를 읽어오는 등의 작업을 수행할 수 있다.
  3. 주기적인 반복
    • 폴링 방식은 지속적으로 장치의 상태를 확인하고 이벤트를 처리하는 과정을 반복한다.
    • 이는 장치의 상태가 변경되거나 이벤트가 발생할 때까지 계속한다.

 

  • HW / SW 인터럽트에 대해 설명해 주세요.

 

3. HW / SW 인터럽트

  1. 하드웨어 인터럽트:
    • 하드웨어 인터럽트는 주로 하드웨어 장치에서 발생하는 이벤트로, 외부에서 발생한 신호에 의해 컴퓨터 시스템의 CPU에 전달된다.
    • 이러한 인터럽트는 하드웨어 장치의 상태 변화를 나타내며, 예를 들어 데이터 전송 완료, 타이머 만료, 키보드 입력 등이 해당한다.
    • 하드웨어 인터럽트는 주로 입출력 장치와의 상호 작용에 사용되며, 장치의 상태 변화를 즉시 처리할 수 있도록 한.
  2. 소프트웨어 인터럽트:
    • 소프트웨어 인터럽트는 주로 프로그램 내에서 발생하는 이벤트로, CPU가 현재 실행 중인 명령어에 의해 생성된다.
    • 예를 들어, 프로그램이 시스템 콜을 호출하거나 예외 상황(예: 나누기 오류)이 발생할 때 소프트웨어 인터럽트가 발생한다.
    • 소프트웨어 인터럽트는 주로 프로세스 관리, 예외 처리, 시스템 호출 등과 관련된 작업에 사용된다.

 

4. 동시에 두 개 이상의 인터럽트가 발생할 때 처리방법

  1. 인터럽트 우선 순위:
    • 각 인터럽트에는 우선 순위가 할당되어 있고, 더 높은 우선 순위를 갖는 인터럽트가 먼저 처리된다.
    • 이는 하드웨어나 소프트웨어에서 지정된 방식으로 우선 순위가 관리된다.
    • 따라서 우선 순위가 높은 인터럽트가 먼저 처리되고, 그 후에 낮은 우선 순위의 인터럽트가 처리된다.
  2. 인터럽트 마스킹:
    • 시스템은 한 번에 하나의 인터럽트만 처리할 수 있기 때문에, 다중 인터럽트가 발생하면 다른 인터럽트를 일시적으로 마스크하여 처리 중인 인터럽트를 완료한 후에 처리할 수 있다.
    • 이를 통해 다중 인터럽트에 대한 우선 순위나 순서를 관리할 수 있다.
  3. 인터럽트 대기 큐:
    • 시스템은 인터럽트를 대기하는 큐를 유지하여 동시에 발생한 인터럽트를 처리하는 방식이다.
    • 인터럽트가 발생하면 해당 인터럽트를 큐에 추가하고, 우선 순위에 따라 큐에서 처리된다.
    • 이 방식은 다중 인터럽트를 효율적으로 처리할 수 있도록 한다.
728x90

'기술 면접 > 운영체제' 카테고리의 다른 글

가상메모리란?  (0) 2024.05.15
프로세스란?  (0) 2024.05.09
시스템 콜이란?  (0) 2024.05.09
728x90

시스템 콜이란 무엇인가?

 

시스템 콜(system call)은 운영 체제의 커널이 제공하는 서비스에 대해, 응용 프로그램의 요청에 따라 커널에 접근하기 위한 인터페이스이다.

 

 

운영체제는 커널모드(Kernel Mode)와 사용자 모드(User Mode)로 나누어진다.

위 사진과 같이 프로그램이 실행되는 것에 있어 파일을 읽고 쓰기, 출력하기 같은 기능을 커널모드를 사용하여 실행한다.

 

운영체제는 다양한 유저의 요구사항에 맞춰 하드웨어를 직접적으로 관리한다.

하지만 유저가 직접적으로 하드웨어를 접근하면 치명적인 오류를 일으킬 수 있다.

 

이를 방지하기 위해 응용 프로그램은 운영체제가 제공하는 인터페이스를 통해서만 하드웨어를 사용할 수 있다.

이러한 인터페이스를 시스템 콜이라고 한다.

 

1. 우리가 사용하는 시스템 콜의 예시

  • 파일 시스템 관련 시스템 콜 : 파일을 열거나 읽고 쓰고 닫기
  • 프로세스 관리 시스템 콜 : 프로세스 생성 / 새로운 프로그램 실행 / 프로세스 종료
  • 통신 관련 시스템 콜 : 소켓 생성 / 연결 요청/수락
  • 메모리 관리 시스템 콜 : 동적 메모리 할당 및 해제
  • 시간 및 날짜 관련 시스템 콜 : 현재 시간 가져오기
  • 디바이스 관리 시스템 콜 : 장치로부터 데이터를 읽거나 쓰기

 

2. 시스템 콜 실행 과정

  1. 사용자 프로그램에서 시스템 콜 호출
    • 사용자 프로그램이 운영 체제 기능을 요청하기 위해 해당 시스템 콜을 호출한다.
    • 이 호출은 일반적으로 프로그래밍 언어의 함수 호출과 유사한 형태를 가지고 있다.
  2. 커널 모드로 전환
    • 사용자 모드(user mode)에서 실행 중인 프로그램이 운영 체제의 권한이 필요한 기능을 호출하면, CPU는 커널 모드(kernel mode)로 전환된다.
    • 이는 보안을 유지하기 위해 필요하다.
  3. 인터럽트 발생
    • 사용자 프로그램이 시스템 콜을 호출하면, 인터럽트(Interrupt)가 발생하여 CPU의 제어를 운영 체제로 넘깁니다.
    • 이는 사용자 모드에서 커널 모드로 전환하고, 이 과정에서 특별한 인터럽트 번호나 지정된 레지스터 값이 전달된다.
    • 자세한건 이후 포스트에서.
  4. 시스템 콜 핸들러 실행
    • 운영 체제는 해당 시스템 콜을 처리하기 위한 적절한 핸들러 함수를 실행한다.
    • 이 핸들러 함수는 요청된 작업을 수행하고, 필요한 경우에는 다시 사용자 모드로 컨텍스트를 전환한.
  5. 작업 수행
    • 시스템 콜 핸들러는 요청된 작업을 수행한다.
    • 이는 파일을 읽거나 쓰는 것, 프로세스를 생성하는 것 등 다양하다.
  6. 결과 반환
    • 작업이 완료되면, 결과가 사용자 프로그램에 반환됩니다.
    • 이를 통해 프로그램은 계속 실행을 진행할 수 있다.
  7. 커널 모드에서 사용자 모드로 복귀
    • 작업이 완료되고 결과가 반환되면, CPU는 다시 사용자 모드로 전환된다.
    • 이후 프로그램은 원래의 실행 위치로 돌아가 계속 실행된다.

 

 

3. 시스템 콜 유형

시스템 콜은 주로 다음과 같은 네 가지 유형으로 구분된다.

  1. 프로세스 제어(System Calls for Process Control):
    • 프로세스 생성, 종료, 프로세스 간 통신, 그리고 프로세스 스케줄링과 관련된 시스템 콜이 있다.
    • 예시: fork(), exec(), exit(), wait(), kill()
  2. 파일 관리(System Calls for File Management):
    • 파일 및 디렉토리를 생성, 열기, 읽기, 쓰기, 삭제하고 파일 속성을 조작하는 시스템 콜이 있다.
    • 예시: open(), read(), write(), close(), mkdir(), rmdir()
  3. 장치 관리(System Calls for Device Management):
    • 하드웨어 및 입출력 장치와 상호 작용하기 위한 시스템 콜이 있다.
    • 예시: ioctl(), read(), write()
  4. 정보 관리(System Calls for Information Management):
    • 운영 체제 및 시스템 정보에 접근하고 관리하는 시스템 콜이 있다.
    • 예시: time(), getpid(), getuid(), getgid()

 

4. 운영체제의 Dual Mode

이중 동작 모드는 쉽게 말해 운영체제를 보호하기 위한 기법이다.

사용자와 운영체제는 시스템 자원을 공유하기 때문에, 사용자에게 제한을 두지 않으면 사용자가 메모리 내의 주요 운영체제 자원을 망가뜨릴 수도 있다.

이러한 점을 보호하기 위해 필요한 보호 장치가 이중 동작 모드(Dual Mode)이다.

위 그림을 보면 이중 동작 모드는 사용자 모드와 커널 모드로 구성되어 있다.

이들은 각각 사용자를 위한 동작과 운영체제를 위한 동작을 담당한다.

이 덕분에 사용자 모드에서는 불법적인 명령을 하지 못하게 한다.

 

중요성

만약 이중동작 모드가 없다면, 잘못된 사용자 프로그램이 데이터를 운영체제 부분에 덮어 기록하게 된다면?

그럼 운영체제가 날라가는거다.

그만큼 중요한 기능이다.

 

 

5. 시스템 콜 구분 방법

  1. 목적에 따른 구분:
    • 프로세스 관리(System Calls for Process Management): 프로세스 생성, 종료, 스케줄링 등과 관련된 시스템 콜.
    • 파일 관리(System Calls for File Management): 파일 및 디렉토리 관리, 파일 읽기 및 쓰기 등과 관련된 시스템 콜.
    • 메모리 관리(System Calls for Memory Management): 동적 메모리 할당 및 해제 등과 관련된 시스템 콜.
    • 장치 관리(System Calls for Device Management): 입출력 장치와의 상호 작용과 관련된 시스템 콜.
  2. 인터페이스에 따른 구분:
    • 유저 레벨 시스템 콜(User-Level System Calls): 사용자 레벨에서 호출되는 시스템 콜. 이들은 직접 운영 체제의 커널에 접근하지 않고, 라이브러리 함수를 통해 호출됩니다.
    • 커널 레벨 시스템 콜(Kernel-Level System Calls): 커널 내부에서 실행되는 시스템 콜. 이들은 사용자 프로세스가 운영 체제 커널에 직접 요청하는 것으로, 보안 및 권한 관리를 위해 커널 모드에서만 실행됩니다.
  3. 기능에 따른 구분:
    • 입출력 관리(System Calls for I/O Management): 파일, 네트워크, 디바이스 등과의 입출력 관련 시스템 콜.
    • 프로세스 제어(System Calls for Process Control): 프로세스 생성, 종료, 신호 처리 등과 관련된 시스템 콜.
    • 자원 관리(System Calls for Resource Management): 메모리 할당, 파일 및 디렉토리 생성, 삭제 등과 관련된 시스템 콜.
728x90

'기술 면접 > 운영체제' 카테고리의 다른 글

가상메모리란?  (0) 2024.05.15
프로세스란?  (0) 2024.05.09
인터럽트란?  (0) 2024.05.09
728x90

1. 상속 관계 매핑

관계형 DB에는 객체지향 언어에서 다루는 상속이라는 개념이 없다.

출처 : 자바 ORM 표준 JPA 프로그래밍

 대신에 위 그림과 같이 슈퍼타입 서브타입 관계라는 모델링 기법이 객체의 상속 개념과 가장 유사하다.

ORM에서 이야기 하는 상속 관계 매핑은 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑하는 것이다.

 

슈퍼타입 서브타입 논리 모델을 실제 물리 모델인 테이블로 구현할 때는 3가지 방법을 선택할 수 있다.

 

더보기

1.1 각각의 테이블로 변환

출처 : 자바 ORM 표준 JPA 프로그래밍

각각을 모두 테이블로 만들고 조회할 때 조인을 사용한다.

JPA에서는 조인 전략이라고 한다.

 

엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략이다.

따라서 조회할 때 조인을 자주 사용한다.

이 전략을 사용할 때 주의할 점이 있는데 객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없다.

따라서 타입을 구분하는 DTYPE 컬럼을 구분 컬럼으로 사용한다.

 

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item
{
    @Id
    @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;
    
    private String name;
    private int price;
    ...
}

@Entity
@DiscriminatorValue("A")
public class Album extends Item
{
    private String artist;
    ...
}

@Entity
@DiscriminatorValue("M")
public class Movie extends Item
{
    private String director;
    private String actor;
    ...
}

 

위 코드를 분석해보자

 

01. @Inheritance(strategy = InheritanceType.JOINED)

상속 매핑은 부모 클래스에 @Inheritance를 사용해야 한다.

그래고 매핑 전략을 지정해야 하는데 여기서는 조인 적략을 사용하므로 InheritanceType.JOINED를 사용했다.

 

02. @DiscriminatorColumn(name = "DTYPE") 

부모 클래스에 구분 컬럼을 지정한다.

이 컬럼으로 저장된 자식 테이블을 구분할 수 있다.

기본값이 DTYPE이므로 @DiscriminatorColumn으로 줄여서 사용해도 된다.

 

03. @DiscriminatorValue("M")

엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다.

만약 영화 엔티티를 저장하면 구분 컬럼인 DTYPE에 값 M이 저장된다.

 

기본 값으로 자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용하는데, 만약 자식 테이블의 기본 키 컬럼명을 변경하고 싶으면 @PrimaryKeyJoinColumn을 사용하면 된다.

 

@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID") // ID 재정의
public class Book extends Item
{
    private String author;
    private String isbn;
    ...
}

BOOK 테이블의 ITEM_ID 기본 키 컬럼명을 BOOK_ID로 변경했다.

 

장점

  • 테이블이 정규화된다.
  • 외래 키 참조 무결성 제약조건을 활용할 수 있다.
  • 저장공간을 효율적으로 사용한다.

단점

  • 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.
  • 조회 쿼리가 복잡하다.
  • 데이터를 등록할 INSERT SQL을 두 번 실행한다.

특징

  • JPA 표준 명세는 구분 컬럼을 사용하도록 하지만 하이버네이트를 포함한 몇몇 구현체는 구분컬럼(@DiscriminatorColumn)없이도 동작한다.

 

 

02. 통합 테이블로 변환

테이블을 하나만 사용해서 통합한다.

JPA에서는 단일 테이블 전략이라고 한다.

출처 : 자바 ORM 표준 JPA 프로그래밍

위 그림과 같이 이름 그대로 테이블을 하나만 사용한다.

구분 컬럼(DTYPE)으로 어떤 자식 데이터가 저장되었는지 구분한다.

조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠르다.

 

이 전략을 사용할 때 주의점은 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.

예를 들어 Book 엔티티를 저장하면 ITEM 테이블의 AUTHOR, ISBN 컬럼만 사용하고 다른 엔티티와 매핑 된 ARTIST. DIRECTOR, ACTOR 컬럼은 사용하지 않으므로 null이 입력되기 때문이다.

 

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item
{
    @Id
    @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;
    
    private String name;
    private int price;
    ...
}

@Entity
@DiscriminatorValue("A")
public class Album extends Item { ... }


@Entity
@DiscriminatorValue("M")
public class Movie extends Item { ... }


@Entity
@DiscriminatorValue("B")
public class Book extends Item { ... }

 

InheritanceType.SINGLE_TABLE 로 지정하면 단일 테이블 전략을 사용한다.

테이블 하나에 모든 것을 통합하므로 구분 컬럼을 필수로 사용해야 한다.

단일 테이블 전략의 장단점은 하나의 테이블을 사용하는 특징과 관련 있다.

 

장점

  • 조인이 필요 없으므로 일반적으로 조회 성능이 빠르다.
  • 조회 쿼리가 단순하다.

단점

  • 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
  • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다.
  • 그러므로 상황에 따라서는 조회 성능이 오히려 느려질 수 있다.

특징

  • 구분 컬럼을 꼭 사용해야 한다. 따라서 @DiscriminatorColumn을 꼭 설정해야 한다.
  • @DiscriminatorValue를 지정하지 않으면 기본으로 엔티티 이름을 사용한다.

03. 서브타입 테이블로 변환

서브 타입마다 하나의 테이블을 만든다.

JPA에서는 구현 클래스마다 테이블 전략이라 한다.

출처 : 자바 ORM 표준 JPA 프로그래밍

위 그림과 같이 자식 엔티티마다 테이블을 만든다.

그리고 자식 테이블 각각에 필요한 컬럼이 모두 있다.

 

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item
{
    @Id
    @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;
    
    private String name;
    private int price;
    ...
}

@Entity
public class Album extends Item { ... }

@Entity
public class Movie extends Item { ... }

@Entity
public class Book extends Item { ... }

InheritanceType.TABLE_PER_CLASS를 선택하면 구현 클래스마다 테이블 전략을 사용한다.

이 전략은 자식 엔티티마다 테이블을 만든다.

일반적으로 추천하지 않는 전략이다.

 

장점

  • 서브 타입을 구분해서 처리할 때 효과적이다.
  • not null 제약조건을 사용할 수 있다.

단점

  • 여러 자식 테이블을 함께 조회할 때 성능이 느리다(SQL에 UNION을 사용해야 한다.)
  • 자식 테이블을 통합해서 쿼리하기 어렵다.

특징

  • 구분 컬럼을 사용하지 않는다.

 

이 전략은 DB 설계자와 ORM 전문가 둘 다 추천하지 않는 전략이다.

조인이나 단일 테이블 전략을 고려하자.

2. @MappedSuperclass

더보기

지금까지 학습한 상속 관계 매핑은 부모 클래스와 자식 클래스를 모두 DB 테이블과 매핑했다.

부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만 제공하고 싶으면 @MappedSuperclass를 사용하면 된다.

 

@MappedSuperclass는 비유를 하자면 추상 클래스와 비슷한데 @Entity는 실제 테이블과 매핑되지만 @MappedSuperclass는 실제 테이블과는 매핑되지 않는다.

이것은 단순히 정보를 상속할 목적으로만 사용된다.

출처 : 자바 ORM 표준 JPA 프로그래밍
출처 : 자바 ORM 표준 JPA 프로그래밍

회원(Member)과 판매자(Seller)는 서로 관계가 없는 테이블과 엔티티이다.

@MappedSuperclass
public abstract class BaseEntity
{
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    ...
}

@Entity
public class Member extends BaseEntity
{
    // ID 상속
    // NAME 상속
    private String email;
    ...
}

@Entity
public class Seller extends BaseEntity
{
    // ID 상속
    // NAME 상속
    private String shopName;
    ...
}

테이블은 그대로 두고 객체 모델의 id, name 두 공통 속성을 부모 클래스로 모으고 객체 상속 관계로 만든 코드이다

 

BaseEntity에는 객체들이 주로 사용하는 공통 매핑 정보를 정의했다.

그리고 자식 엔티티들은 상속을 통해 BaseEntity의 매핑 정보를 물려받았다.

여기서 BaseEntity는 테이블과 매핑할 필요가 없고 자식 엔티티에게 공통으로 사용되는 매핑 정보만 제공하면 된다.

따라서 @MappedSuperclass를 사용했다.

 

부모로부터 물려받은 매핑 정보를 재정의하려면 @AttributedOverrides나 @AttributeOverride를 사용하고, 연관관계를 재정의하려면  @AssociationOverrides나 @AssociationOverride를 사용한다.

 

@Entity
@AttributeOverrides({
        @AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID")),
        @AttributeOverride(name = "name", column = @Column(name = "MEMBER_NAME"))
})
public class Member extends BaseEntity { ... }

 

정리

  • 테이블과 매핑되지 않고 자식 클래스에 엔티티에 매핑 정보를 상속하기 위해 사용한다.
  • @MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 em.find()나 JPQL에서 사용할 수 없다.
  • 이 클래스를 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것을 권장한다.
  • @MappedSuperclass를 사용하면 등록일자, 수정일자, 등록자, 수정자 같은 여러 엔티티에서 공통으로 사용하는 속성을 효과적으로 관리할 수 있다.

3. 복합 키와 식별 관계 매핑

더보기

01. 식별관계 vs 비식별 관계

DB 테이블 사이에 관계는 외래 키가 기본 키에 포함되는지 여부에 따라 식별 관계와 비식별 관계로 구분한다.

 

식별 관계

식별 관계는 부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 관계다.

출처 : 자바 ORM 표준 JPA 프로그래밍

위 그림을 보면 PARENT 테이블의 기본 키 PARENT_ID를 받아서 CHILD 테이블의 기본 키(PK) + 외래 키(FK)로 사용한다.

 

비식별 관계

비식별 관계는 부모 테이블의 기본 키를 받아서 자식의 테이블의 외래 키로만 사용하는 관계이다.

출처 : 자바 ORM 표준 JPA 프로그래밍

위 그림을 보면 PARENT 테이블의 기본 키 PARENT_ID를 받아서 CHILD 테이블의 외래 키(FK)로만 사용한다.

 

비식별 관계는 외래 키에 NULL을 허용하는지에 따라 필수적 비식별 관계와 선택적 비식별 관계로 나눈다.

  • 필수적 비식별 관계(Mandatory) : 외래 키에 NULL을 허용하지 않는다. 연관관계를 필수적으로 맺어야 한다.
  • 선택적 비식별 관계(Optional) : 외래 키에 NULL을 허용한다. 연관관계를 맺을지 말지 선택할 수 있다.

DB 테이블을 설계할 때 식별 관계나 비식별 관계 중 하나를 선택해야 한다.

최근에는 비식별 관계를 주로 사용하고 꼭 필요한 곳에만 식별 관계를 사용하는 추세다.

JPA는 식별관계와 비식별 관계를 모두 지원한다.

 

식별 관계와 비식별 관계를 어떻게 매핑하는지 알아보자.

 

02. 복합키 : 비식별 관계 매핑

@Entity

public class Hello
{

    @Id
    private String id;
}

기본 키를 구성하는 컬림이 하나면 위 코드처럼 단순하게 매핑한다.

@Entity

public class Hello
{

    @Id
    private String id1;
    
    @Id
    private String id2; // 실행 시점에 매핑 예외 발생
}

둘 이상의 컬럼으로 구성된 복합 기본 키는 위 코드처럼 매핑하면 될 것 같지만 막상 해보면 매핑 오류가 발생한다.

JPA에서 식별자를 둘 이상 사용하려면 별도의 식별자 클래스를 만들어야한다.

 

JPA는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용한다.

그리고 식별자를 구분하기 위해 equals와 hashCode를 사용해서 동등성 비교를 한다.

그런데 식별자 필드가 하나일 때는 보통 자바의 기본 타입을 사용하므로 문제가 없지만, 식별자 필드가 2개 이상이면 별도의 식별자 클래스를 만들고 그곳에 equals와 hashCode를 구현해야 한다.

 

JPA는 복합 키를 지원하기 위해 @IdClass와 @EmbeddedId 2가지 방법을 제공하는데 @IdClass는 관계형 DB에 가까운 방법이고 @EmbeddedId는 좀 더 객체지향에 가까운 방법이다.

 

 

@IdClass

자바 ORM 표준 JPA 프로그래밍

PARENT 테이블을 보면 기본 키를 PARENT_ID1, PARENT_ID2로 묶은 복합 키로 구성했다.

따라서 복합 키를 매핑하기 위해 식별자 클래스를 별도로 만들어야 한다.

 

부모 클래스

@Entity
@IdClass(ParentId.class)
public class Parent
{
    @Id
    @Column(name = "PARENT_ID1")
    private String id1; // ParentID.id1 과 연결
    
    @Id
    @Column(name = "PARENT_ID2")
    private String id2; // ParentID.id2 와 연결
    
    private String name;
    ...
}

 

식별자 클래스

public class ParentId implements Serializable
{
    private String id1; // Parent.id1 매핑
    private String id2; // Parent.id2 매핑
    
    public ParentId() {}
    
    public ParentId(String id1, String id2)
    {
        thid.id1 = id1;
        this.id2 = id2;
    }
    
    @Override
    public boolean equals(Object o) { ... }
    
    @Override
    public int hashCode() { ... }
}

PARENT 테이블을 매핑한 코드이다.

각각의 기본 키 컬럼을 @Id로 매핑했다.

그리고 @IdClass를 사용해서 ParentId 클래스를 식별자 클래스로 지정했다.

 

@Entity
public class Child
{
    @Id
    private String id;
    
    @ManyToOne
    @JoinColumns({
            @JoinColumn(name = "PARENT_ID1",
                referencedColumnName = "PARENT_ID1"),
            @JoinColumn(name = "PARENT_ID2",
                referencedColumnName = "PARENT_ID2")
    }
    private Parent parent;
}

부모 테이블의 기본 키 컬럼이 복합 키 이므로 자식 테이블의 외래 키도 복합 키다.

따라서 외래 키 매핑 시 여러 컬럼을 매핑해야 하므로 @JoinColumns 어노테이션을 사용하고 각각의 외래 키 커럼을 @JoinColumn으로 매핑한다.

 

참고로 위 코드처럼 JoinColumn의 name 속성과 referencedColumnName 속성의 값이 같으면 referencedColumnName은 생략해도 된다.

 

 

@EmbeddedId

@IdClass가 DB에 맞춘 방법이라면 @EmbeddedId는 좀 더 객체지향적인 방법이다.

@Entity
public class Parent
{
    @EmbeddedId
    private ParentId id;
    
    private String name;
    ...
}

Parent 엔티티에서 식별자 클래스를 직접 사용하고 @EmbeddedId 어노테이션을 적어주면 된다.

@Embeddable
public class ParentId implements Serializable
{
    @Column(name = "PARENT_ID1")
    private String id1;
    @Column(name = "PARENT_ID2")
    private String id2;
    
    // equals and hashCode 구현
    ...
}

@IdClass와는 다르게 @EmbeddedId를 적용한 식별자 클래스는 식별자 클래스에 기본 키를 직접 매핑한다.

@EmbeddedId를 적용한 식별자 클래스는 다음 조건을 만족해야한다.

  • @Embeddable 어노테이션을 붙여주어야 한다.
  • Serializable 인터페이스를 구현해야 한다.
  • equals, hashCode를 구현해야 한다.
  • 기본생성자가 있어야 한다.
  • 식별자 클래스는 public이어야한다.
Parent parent = new Parent();
ParentId parentId = new ParentId("myId1", "myId2");
parent.setId(parentId);
parent.setName("parentName");
em.persist(parent);

저장하는 코드이다.

식별자 클래스 parentId를 직접 생성해서 사용한다.

ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);

조회 코드도 식별자 클래스 parentId를 직접 사용한다.

 

 

복합 키와 equals(), hashCode()

복합 키는 equals()와 hashCode()를 필수로 구현해야 한다.

ParentID id1 = new parentId();
id1.setId1("myId1");
id1.setId2("myId2");

ParentID id2 = new parentId();
id2.setId1("myId1");
id2.setId2("myId2");

id1.equals(id2) // ??

이것은 순수한 자바 코드이다.

id1과 id2 인스턴스 둘 다 myId1, myId2라는 같은 값을 가지고 있지만 인스턴스는 다르다.

그래서 오버라이딩을 하지 않으면 마지막 줄은 거짓이다.

그 이유는 기본 equals()는 인스턴스 참조 값 비교인 ==비교(동일성 비교)를 하기 때문이다.

 

영속성 컨텍스트는 엔티티의 식별자를 키로 사용해서 엔티티를 관리한다.

기르고 식별자를 비교할 때 equals() 와 hashCode()를 사용한다.

따라서 식별자 객체의 동등성이 지겨지지 않으면 예상과 다른 엔티티가 조회되거나 엔티티를 찾을수 없는 등 심각한 문제가 발생한다.

따라서 복합 키는 equals()와 hashCode()를 필수로 구현해야 한다.

 

03. 복합 키 : 식별 관계 매핑

출처 : 자바 ORM 표준 JPA 프로그래밍

위 그림은 부모, 자식, 손자까지 계속 기본 키를 전달하는 식별 관계다.

식별 관계에서 자식 테이블은 부모 테이블의 기본 키를 포함해서 복합 키를 구성해야 하므로 @IdClass나 @EmbeddedId를 사용해서 식별자를 매핑해야 한다.

 

@IdClass와 식별 관계

 

부모 엔티티

// 부모
@Entity
public class Parent
{
    @Id
    @Column(name = "PARENT_ID")
    private String id;
    private String name;
    ...
}

 

자식 엔티티

// 자식
@Entity
@IdClass(ChildId.class)
public class Child
{
    @Id
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    public Parent parent;
    
    @Id
    @Column(name = "CHILD_ID")
    private String childId;
    
    private String name;
    ...
}

// 자식 ID
public class ChildId implements Serializable
{
    private String parent; // Child.parent 매핑
    private String childId; // Child.childId 매핑
    
    // equals, hashCode
    ...
}

 

손자 엔티티

// 손자
@Entity
@IdClass(GrandChildId.class)
public class GrandChild
{
    @Id
    @ManyToOne
    @JoinColumn({
            @JoinColumn(name = "PARENT_ID"),
            @JoinColumn(name = "CHILD_ID")
    }
    private Child child;
    
    @Id
    @Column(name = "GRANDCHILD_ID")
    private String id;
    
    private String name;
    ...
}

// 손자 ID
public class GrandChildId implements Serializable
{
    private ChildId child; // GrandChild.child 매핑
    private String id; // GrandChild.id 매핑
    
    // equals, hashCode
    ...
}

 

식별 관계는 기본 키와 외래 키를 같이 매핑해야 한다.

@Id
@ManyToOne
@JoinColumn(name = "PARENT_ID")
public Parent parent;

따라서 식별자 매핑인 @Id와 연관관계 매핑인 @ManyToOne을 같이 사용하면 된다.

Child 엔티티의 parent 필드를 보면 @Id로 기본 키를 매핑하면서 @ManyToOne과 @JoinColumn으로 외래 키를 같이 매핑한다.

 

 

@EmbeddedId와 식별 관계

@EmbeddedId로 식별 관계를 구성할 때는 @MapsId를 사용해야 한다.

 

부모 엔티티

// 부모
@Entity
public class Parent
{
    @Id
    @Column(name = "PARENT_ID")
    private String id;
    
    private String name;
    ...
}

 

자식 엔티티

// 자식
@Entity
public class Child
{
    @EmbeddedId
    private ChildId id;
    
    @MapsId("parentId") // ChildId.parentId 매핑
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    public Parent parent;
    
    private String name;
    ...
}

//자식 ID
@Embeddable
public class ChildId implements Serializable
{
    private String parentId; // @MapsId("parentId")로 매핑
    
    @Column(name = "CHILD_ID")
    private String id;
    
    // equals, hashCode
    ...
}

 

손자 엔티티

// 손자
@Entity
public class GrandChild
{
    @EmbeddedId
    private GrandChildId id;
    
    @MapsId("childId") // GrandChildId.childId 매핑
    @ManyToOne
    @JoinColumns({
            @JoinColumn(name = "PARENT_ID"),
            @JoinColumn(name = "CHILD_ID")
    })
    private Child child;
    
    private String name;
    ...
}

// 손자 ID
@Embeddable
public class GrandChildId implements Serializable
{
    private ChildId childId; // @MapsId("childId")로 매핑
    
    @Column(name = "GRANDCHILD_ID")
    private String id;
    
    // equals, hashCode
    ...
}

 

@EmbeddedID는 식별 관계로 사용할 연관관계의 속성에 @MapsId를 사용하면 된다.

@MapsId("parentId")
@ManyToOne
@JoinColumn(name = "PARENT_ID")
public Parent parent;

@IdClass와 다른 점은 @Id 대신에 @MapsId를 사용한 점이다.

@MapsId는 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻이다.

@MapsId의 속성 값은 @EmbeddedId를 사용한 식별자 클래스의 기본 키 필드를 지정하면 된다.

 

04. 비식별 관계로 구현

출처 : 자바 ORM 표준 JPA 프로그래밍

 

부모 엔티티

@Entity
public class Parent
{
    @Id
    @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;
    private String name;
    ...
}
@Entity
public class Child
{
    @Id
    @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;
    private String name;
    
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    private Parent parent;
    ...
}
@Entity
public class GrandChild
{
    @Id
    @GeneratedValue
    @Column(name = "GRANDCHILD_ID")
    private Long id;
    private String name;
    
    @ManyToOne
    @JoinColumn(name = "CHILD_ID")
    private Child child;
    ...
}

 

식별 관계의 복합 키를 사용한 코드와 비교하면 매핑도 쉽고 코드도 단순하다.

그리고 복합 키가 없으므로 복합 키 클래스를 만들지 않아도 된다.

 

05. 일대일 식별 관계

 

출처 : 자바 ORM 표준 JPA 프로그래밍

위 그림에서 일대일 식별 관계는 자식 테이블의 기본 키 값으로 부모 테이블의 기본 키 값만 사용한다.

그래서 부모 테이블의 기본 키가 복합 키가 아니면 자식 테이블의 기본 키는 복합 키로 구성하지 않아도 된다.

// 부모
@Entity
public class Board
{
    @Id
    @GeneratedValue
    @Column(name = "BOARD_ID")
    private Long id;
    
    private String title;
    
    @OneToOne(mappedBy = "board")
    private BoardDetail boardDetail;
    ...
 }
 
 // 자식
 @Entity
 public class BoardDetail
 {
     @Id
     private Long boardId;
     
     @MapsId // BoardDetail.boardId 매핑
     @OneToOne
     @JoinColumn(name = "BOARD_ID")
     private Board board;
     
     private String content;
     ...
}

 

BoardDetail처럼 식별자가 단순히 컬럼 하나면 @MapsId를 사용하고 속성 값은 비워두면 된다.

이때 @MapsId는 @Id를 사용해서 식별자로 지정한 BoardDetail.boardId와 매핑된다.

public void save()
{
    Board board = new Board();
    board.setTitle("제목");
    em.persist(board);
    
    BoardDetail boardDetail = new BoardDetail();
    boardDetail.setContent("내용");
    boardDetail.setBaord(board);
    em.persist(boardDetail);
}

저장하는 코드.

 

06. 식별, 비식별 관계의 장단점

 

DB 설계 관점에서 보면 다음과 같은 이유로 식별 관계보다는 비식별 관계를 선호한다.

  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어난다. 때문에 조인할 때 SQL이 복잡해지고 기본 키 인텍스가 불필요하게 커질 수 있다.
  • 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다.
  • 식별 관계를 사용할 때 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많다. 반면에 비식별 관계의 기본 키는 비즈니스와 전혀 관계없는 대리 키를 주로 사용한다. 비즈니스 요구사항은 시간이 지마넹 따라 언젠가는 변한다. 식별 관계으 ㅣ자연 키 컬럼들이 자식에 손자까지 전파되면 변경하기 힘들다.
  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블의 키본 키로 사용하므로 비식별 관계보다 테이블 구조가 유연하지 못하다.

객체 관계 매핑의 관점에서 보면 다음과 같은 이유로 비식별 관계를 선호한다.

  • 일대일 관계를 제외하고 식별 관계는 2개 이상의 컬럼을 묶은 복합 기본 키를 사용한다. JPA에서 복합 키는 별도의 복합 키 클래스를 만들어서 사용해야 한다. 따라서 컬럼이 하나인 기본 키를 매핑하는 것보다 많은 노력이 필요하다.
  • 비식별 관계의 기본 키는 주로 대리 키를 사용하는데 JPA는 @GenerateValue처럼 대리 키를 생성하기 위한 편리한 방법을 제공한다.

물론 식별 관계가 가지는 장점도 있다.

기본 키 인덱스를 활용하기 좋고, 상위 테이블들의 기본 키 컬럼을 자식, 손자 테이블들이 가지고 있으므로 특정 상황에 조인 없이 하위 테이블만으로 검색을 완료할 수 있다.

4. 조인 테이블

더보기

DB 테이블의 연관관계를 설계하는 방법은 크게 2가지다.

  • 조인 컬럼 사용(외래 키)
  • 조인 테이블 사용(테이블 사용)

조인 컬럼 사용

테이블 간에 관계는 주로 조인 컬럼이라 부르는 외래 키 컬럼을 사용해서 관리한다.

출처 : 자바 ORM 표준 JPA 프로그래밍

회원과 사물함이 있다.

각각 테이블에 데이터를 등록했다가 회원이 원할 때 사물함을 선택할수 있다는 가정.

회원이 사물함을 사용하기 전까지는 아직 둘 사이에 관계가 없으므로 MEMBER 테이블의 LOCKER_ID 외래 키에 null을 입력해두어야 한다.

이렇게 외래 키에 null을 허용하는 관계를 선택적 비식별 관계라 한다.

출처 : 자바 ORM 표준 JPA 프로그래밍

선택적 비식별 관계는 외래 키에 null을 허용하므로 회원과 사물함을 조인할 때 외부 조인을 사용해야 한다.

실수로 내부 조인을 사용하면 사물함과 관계가 없는 회원은 조회되지 않는다.

그리고 회원과 사물함이 아주 가끔 관계를 맺는다면 외래 키 값 대부분이 null로 저장되는 단점이 있다.

 

조인 테이블 사용

출처 : 자바 ORM 표준 JPA 프로그래밍

이 방법은 조인 테이블이라는 별도의 테이블을 사용해서 연관관계를 관리한다.

위의 조인 컬럼과 비교해보면 조인 컬럼을 사용하는 방법은 단순히 외래 키 컬럼만 추가해서 연관관계를 맺지만

조인 테이블을 사용하는 방법은 연관관계를 관리하는 조인 테이블을 추가하고 여기서 두 테이블의 외래 키를 가지고 연관관계를 관리한다.

따라서 MEMBER와 LOCKER에는 연관관계를 관리하기 위한 외래 키 컬럼이 없다.

출처 : 자바 ORM 표준 JPA 프로그래밍

위 그림을 보면 회원과 사물함 데이터를 각각 등록했다가 회원이 원할 때 사물함을 선택하면 MEMBER_LOCKER 테이블에만 값을 추가하면 된다.

 

조인 테이블의 가장 큰 단점은 테이블을 하나 추가해야 한다는 점이다.

따라서 관리해야 하는 테이블이 늘어나고 회원과 사물함 두 테이블을 조인하려면 MEMBER_LOCKER 테이블까지 추가로 조인해야 한다.

따라서 기본은 조인 컬럼을 사용하고 필요하다고 판단되면 조인 테이블을 사용하는게 바람직하다.

 

01. 일대일 조인 테이블

 

출처 : 자바 ORM 표준 JPA 프로그래밍

일대일 관계를 만들려면 조인 테이블의 외래 키 컬럼 각각에 총 2개의 유니크 제약조건을 걸어야 한다.

(PARENT_ID는 기본 키이므로 유니크 제약조건이 걸려있다.)

// 부모
@Entity
public class Parent
{
    @Id
    @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;
    private String name;
    
    @OneToOne
    @JoinTable(name = "PARENT_CHILD",
            joinColumns = @JoinColumn(name = "PARENT_ID"),
            inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
    )
    private Chile child;
    ...
}

// 자식
@Entity
public class Child
{
    @Id
    @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;
    private String name;
    ...
}

부모 엔티티를 보면 @JoinColumn 대신에 @JoinTable을 사용했다.

@JoinTable의 속성은 다음과 같다.

  • name : 매핑할 조인 테이블 이름
  • joinColumns : 현재 엔티티를참조하는 외래 키
  • inverseJoinColumns : 반대방향 엔티티를 참조하는 외래 키
public class Child
{
    ...
    @OneToOne(mappedBy = "child")
    private Parent parent;
}

양방향으로 매핑하려면 위 코드를 추가하면 된다

 

02. 일대다 조인 테이블

출처 : 자바 ORM 표준 JPA 프로그래밍

위 그림은 일대다 관계이다.

일대다 관계를 만들려면 조인 테이블의 컬럼 중 다(N)와 관련된 컬럼인 CHILD_ID에 유니크 제약조건을 걸어야 한다.

(CHILD_ID는 기본 키 이므로 유니크 제약조건이 걸려있다.)

 

// 부모
@Entity
public class Parent
{
    @Id
    @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;
    private String name;
    
    @OneToMany
    @JoinTable(name = "PARENT_CHILD",
            joinColumns = @JoinColumn(name = "PARENT_ID"),
            inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
    )
    private List<Child> child = new ArrayList<Child>();
    ...
}

// 자식
@Entity
public class Child
{
    @Id
    @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;
    private String name;
    ...
}

 

03. 다대일 조인 테이블

다대일은 일대다에서 방향만 반대이다.

 

04. 다대다 조인 테이블

출처 : 자바 ORM 표준 JPA 프로그래밍

위 그림은 다대다 관계다.

다대다 관계를 만들려면 조인 테이블의 두 컬럼을 합해서 하나의 복합 유니크 제약조건을 걸어야한다.

(PARENT_ID, CHILD_ID는 기본 키이므로 유니크 제약조건이 걸려 있다.)

// 부모
@Entity
public class Parent
{
    @Id
    @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;
    private String name;
    
    @ManyToMany
    @JoinTable(name = "PARENT_CHILD",
            joinColumns = @JoinColumn(name = "PARENT_ID"),
            inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
    )
    private List<Child> child = new ArrayList<Child>();
    ...
}

// 자식
@Entity class Child
{
    @Id
    @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;
    private String name;
    ...
}

 

5. 엔티티 하나에 여러 테이블 매핑

더보기

잘 사용하지는 않지만 @SecondaryTable을 사용하면 한 엔티티에 여러 테이블을 잘 매핑할 수 있다.

출처 : 자바 ORM 표준 JPA 프로그래밍
@Entity
@Table(name = "BOARD")
@SecondaryTable(name = "BOARD_DETAIL",
    pkJoinColumns = @PrimaryKeyJoinColumn(name = "BOARD_DETAIL_ID"))
public class Board
{
    @Id
    @GeneratedValue
    @Column(name = "BOARD_ID")
    private Long id;
    
    private String title;
    
    @Column(table = "BOARD_DETAIL")
    private String content;
    ...
}

Board 엔티티는 @Table을 사용해서 BOARD 테입르과 매핑했다.

그리고 @SecondaryTable을 사용해서 BOARD_DETAIL 테이블을 추가로 매핑했다.

 

@SecondaryTable 속성

  • @SecondaryTable.name :  매핑할 다른 테이블의 이름, 예제에서는 테이블명을 BOARD_DETAIL로 지정했다.
  • @SecondaryTable.pkJoinColumns : 매핑할 다른 테이블의 기본 키 컬럼 속성, 예제에서는 기본 키 컬럼 명을 BOARD_DETAIL_ID로 지정했다. 

content 필드는 @Column(table = "BOARD_DETAIL")을 사용해서 BOARD_DETAIL 테이블의 컬럼에 매핑했다.

title 필드처럼 테이블을 지정하지 않으면 기본 테이블인 BOARD에 매핑된다.

 

더 많은 테이블을 매핑하려면 @SecondaryTables를 사용하면 된다.

@SecondaryTables({
    @SecondaryTable(name = "BOARD_DETAIL"),
    @SecondaryTable(name = "BOARD_FILE")
})

 

@SecondaryTable을 사용해서 두 테이블을 하나의 엔티티에 매핑하는 방법보다는 테이블당 엔티티를 각각 만들어서 일대일 매핑하는 것을 권장한다.

728x90

'JPA' 카테고리의 다른 글

JPA TIL - 15 고급 주제와 성능 최적화  (0) 2024.07.10
JPA TIL - 09 값 타입  (0) 2024.05.15
JPA TIL - 06 다양한 연관관계 매핑  (0) 2024.04.16
JPA TIL - 05 연관관계 매핑 기초  (0) 2024.04.09
JPA TIL - 04 엔티티 매핑  (0) 2024.04.02
728x90

엔티티의 연관관계를 매핑할 때는 다음 3가지를 고려해야 한다.

  • 다중성
  • 단방향, 양방향
  • 연관관계의 주인

1. 다대일

다대일 관계의 반대 방향은 항상 일대다 관계고, 일대다 관계의 반대 방향은 항상 다대일 관계다.

DB 테이블의 일대다 관계에서 외래키는 항상 다쪽에 있다.

예를 들어 회원(N)과 팀(1)이 있으면 항상 회원 쪽이 연관관계의 주인이다.

 

01. 다대일 단방향[N:1]

더보기
자바 ORM 표준 JPA 프로그래밍

 

회원 엔티티

@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 ...
    
}

회원은 Memger.team으로 팀 엔티티를 참조할 수 있지만 반대로 팀에는 회원을 참조하는 필드가 없다.

따라서 회원과 팀은 다대일 단방향 연관관계이다.

 

@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;

@JoinColumn(name = "TEAM_ID")를 사용해서 Member.team 필드를 TEAM_ID 외래 키와 매핑했다.

따라서 Member.team 필드로 회원 테이블의 TEAM_ID 외래키를 관리한다.

 

02. 다대일 양방향[N:1, 1:N]

 

더보기
자바 ORM 표준 JPA 프로그래밍

위 사진에서 실선은 연관관계의 주인이고, 점선은 연관관계의 주인이 아니다.

 

회원 엔티티

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

 

  • 양방향 외래 키가 있는 쪽이 연관관계의 주인이다.

일대다와 다대일 연관관계는 항상 다(N)에 외래 키가 있다.

여기서는 MEMBER 테이블이 외래 키를 가지고 있으므로 Member.team이 연관관계의 주인이다.

JPA는 외래 키를 관리할 때 연관관계의 주인만 사용한다.

주인이 아닌 Team.members는 조회를 위한 JPQL이나 객체 그래프를 탐색할 때 사용한다.

 

  • 양방향 연관관계는 항상 서로를 참조해야 한다

양방향 연관관계는 항상 서로 참조해야 한다.

어느 한 쪽만 참조하면 양방향 연관관계가 성립하지 않는다.

항상 서로 참조하게 하려면 연관관계 편의 메소드를 작성하는 것이 좋은데 회원의 setTeam(), 팀의 addMember() 메소드가 이런 편의 메소드들이다.

편의 메소드는 한 곳에만 작성하거나 양쪽 다 작성할 수 있는데, 양쪽에 다 작성하면 무한루프에 빠지므로 주의해야 한다.


2. 일대다

일대다 관계는 다대일 관계의 반대 방향이다.

일대다 관계는 엔티티를 하나 이상 참조할 수 있으므로 자바 컬렉션인 Collection, List, Set, Map중에 하나를 사용해야 한다.

 

01. 일대다 단방향[1:N]

더보기

하나의 팀은 여러 회원을 참조할 수 있는데 이런 관계를 일대다 관계라고 한다.

팀은 회원들을 참조하지만 반대로 회원은 팀을 참조하지 않으면 둘의 관계는 단방향이다.

자바 ORM 표준 JPA 프로그래밍

일대다 단방향 관계는 약간 특이하다.

위 그림을 보면 팀 엔티티의 Team.members로 회원 테이블의 TEAM_ID 외래 키를 관리한다.

보통 자신이 매핑한 테이블의 외래 키를 관리하는데, 이 매핑은 반대쪽 테이블에 있는 외래 키를 관리한다.

어쩔 수 없는게, 일대다 관계에서 외래 키는 항상 다쪽 테이블에 있다.

따라서 반대편 테이블의 외래 키를 관리하는 특이한 모습이 나타난다.

 

팀 엔티티

@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<Member>();
    
    // Getter, Setter ...
}

 

회원 엔티티

@Entity
public class Member
{
    @Id 
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    // Getter, Setter ...
}

일대다 단방향 관계를 매핑할 때는 @JoinColumn을 명시해야 한다.

그렇지 않으면 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블(Join Table) 전략을 기본으로 사용해서 매핑한다.

 

  • 일대다 단방향 매핑의 단점

일대다 단방향 매핑의 단점은 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 점이다.

본인 테이블에 외래 키가 있으면 엔티티의 저장과 연관관계 처리를 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();
}

위 코드를 실행한 결과 SQL은 다음과 같다.

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=?

 

Member엔티티는 Team엔티티를 모른다.

그리고 연관관계에 대한 정보는 Team엔티티의 members가 관리한다.

따라서 Member 엔티티를 저장할 때는 MEMBER테이블의 TEAM_ID 외래 키에 아무 값도 저장되지 않는다.

대신 Team엔티티를 저장할 때 Team.members의 참조 값을 확인해서 회원 테이블에 있는 TEAM_ID 외래 키를 업데이트한다.

 

결국 일대다 단방향  매핑보다는 다대일 양방향 매핑을 사용하는게 맞다.

일대다 단방향 매핑을 사용하면 엔티티를 매핑한 테이블이 아닌 다른 테이블의 외래 키를 관리해야 한다.

이것은 성능 문제 뿐만 아니라 관리도 부담스럽게 된다.

반면에 다대일 양방향 매핑은 관리해야 하는 외래 키가 본인 테이블에 있어서 위와 같은 문제가 발생하지 않는다.

02. 일대다 양방향[1:N, N:1]

 

더보기

양방향 매핑에서 @OneToMany는 연관관계의 주인이 될 수 없다.

왜냐하면 관계형 DB의 특성상 일대다, 다대일 관계는 항상 다쪽에 외래 키가 있다.

 

그렇다고 해도 일대다 양방향 매핑이 완전히 불가능한 것은 아니다.

일대다 단방향 매핑 반대편에 같은 외래 키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 하나 추가하면 된다.

 

자바 ORM 표준 JPA 프로그래밍

팀 엔티티

@Entity
public class Team
{
    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    private String name;
    
    @OneToMany
    @JoinColumnn(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;
    pricate String username;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
    private Team team;
    
    // Getter, Setter ...
    
}

일대다 단방향 매핑 반대편에 단방향 매핑을 추가했다.

이렇게 되면 둘 다 같은 키를 관리하므로 문제가 발생할 수 있다.

따라서 반대편인 다대일 쪽에 insertable = false, updatable = false로 설정해서 읽기만 가능하게 했다.

 

이 방법은 일대다 양방향 매핑이라기 보다는 단방향 매핑 반대편에 읽기 전용으로 추가해서 양방향처럼 보이도록 하는 방법이다.

그래서 되도록이면 다대일 양방향 매핑을 사용하자.


3. 일대일[1:1]

일대일 관계는 양쪽이 서로 하나의 관계만 가진다.

예를 들어 회원은 하나의 사물함만 사용하고 사물함도 하나의 회원에 의해서만 사용된다.

 

일대일 관계는 다음과 같은 특징이 있다.

  • 일대일 관계는 그 반대도 일대일 관계다.
  • 테이블 관계에서 일대다, 다대일은 항상 다(N)쪽이 외래키를 가질수 있는 반면, 일대일 관계는 둘 중 어느곳이나 외래 키를 가질 수 있다.

테이블은 주 테이블이든 대상 테이블이든 외래 키 하나만 있으면 양쪽으로 조회할 수 있다.

그리고 일대일 관계는 그 반대쪽도 일대일 관계이기 때문에 누가 외래 키를 가질지 선택해야 한다.

 

  • 주 테이블에 외래 키

주 객체가 대상 객체를 참조하는 것처럼 주 테이블에 외래 키를 두고 대상 테이블을 참조한다.

외래 키를 객체 참조와 비슷하게 사용할 수 있어서 객체지향 개발자들이 주로 선호한다.

이 방법의 장점은 주 테이블이 외래 키를 가지고 있으므로 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있다.

 

  • 대상 테이블에 외래 키

전통적인 DB 개발자들은 보통 대상 테이블에 외래 키를 두는 것을 선호한다.

이 방법의 장점은 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있다.

 

01. 주 테이블에 외래 키

더보기

일대일 관계를 구성할 때 객체지향 개발자들은 주 테이블에 외래 키가 있는 것을 선호한다.

JPA도 주 테이블에 외래키가 있으면 좀 더 편리하게 매핑할 수 있다.

 

단방향

MEMBER가 주 테이블이고 LOCKER는 대상 테이블이다.

자바 ORM 표준 JPA 프로그래밍
@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
    @GerneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    
    private String name;
    ...
}

일대일 관계이므로 객체 매핑에 @OneToOne을 사용했고 DB에는 LOCKER_ID 외래 키에 유니크 제약 조건(UNI)을 추가했다.

참고로 이 관계는 다대일 단방향(@ManyToOne)과 거의 비슷하다.

 

다음은 반대 방향을 추가해서 일대일 양방향 관계이다.

자바 ORM 표준 JPA 프로그래밍
@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
    @GerneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    
    private String name;
    
    @OneToOne(mappedBy = "locker")
    private Member member;
    ...
}

양방향이므로 연관관계의 주인을 정해야 한다.

MEMBER 테이블이 외래 키를 가지고 있으므로 Member 엔티티에 있는 Member.locker가 연관관계의 주인이다.

따라서 반대 매핑인 사물함의 Locker.member는 mappedBy를 선언해서 연관관계의 주인이 아니라고 설정했다.

02. 대상 테이블에 외래 키

 

더보기

단방향

자바 ORM 표준 JPA 프로그래밍

일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않는다.

그리고 이런 모양으로 매핑할 수 있는 방법도 없다.

이때는 단방향 관계를 Locker에서 Member방향으로 수정하거나, 양방향 관계로 만들고 Locker를 연관관계의 주인으로 설정해야 한다.

이 방법은 다음 양방향에서 나온다.

(참고로 JPA2.0부터 일대다 단방향 관계에서 대상 테이블에 외래 키가 있는 매핑을 허용했다. 하지만 일대일 단방향은 이런 매핑을 허용하지 않는다.)

 

 

양방향

자바 ORM 표준 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 테이블의 외래 키를 관리하도록 했다.


4. 다대다 [N:N]

자바 ORM 표준 JPA 프로그래밍

관계형 DB는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.

때문에 보통 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다.

 

예를 들어 회원들은 상품을 주문한다.

반대로 상품들은 회원들에 의해 주문된다.

 

둘은 다대다 관계다.

따라서 회원 테이블과 상품 테이블만으로는 이 관계를 표현할 수 없다.

자바 ORM 표준 JPA 프로그래밍

그래서 위 그림처럼 중간에 연결 테이블을 추가해야 한다.

이 테이블을 사용해서 다대다 관계를 일대다, 다대일 관계로 풀어낼 수 있다.

 

자바 ORM 표준 JPA 프로그래밍

그런데 객체는 테이블과 다르게 객체 2개로 다대다 관계를 만들 수 있다.

예를 들어 회원 객체는 컬렉션을 사용해서 상품들을 참조하고,

반대로 상품들도 회원 객체를 컬렉션을 사용해서 참조하면 된다.

 

@ManyToMany를 사용하면 위 그림처럼 다대다 관계를 편리하게 매핑할 수 있다.

 

01. 다대다 : 단방향

더보기

회원 엔티티

@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"), 
        inverseJoinColums = @JoinColum(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 : 연결 테이블을 지정한다. 여기서는 MEMBER_PRODUCT 테이블을 선택했다.
  • @JoinTable.joinColums : 현재 방향인 회원과 매핑할 조인 컬럼 정보를 지정한다. MEMBER_ID로 지정했다.
  • @JoinTable.inverseJoinColumns : 반대 방향인 상품과 매핑할 조인 컬럼 정보를 지정한다. PRODUCT_ID로 지정했다.

MEMBER_PRODUCT 테이블은 다대다 관계를 일대다, 다대일 관계로 풀어내기 위해 필요한 연결 테이블이 뿐이다.

@ManyToMany로 매핑한 덕분에 다대다 관계를 사용할 때는 이 연결 테이블을 신경 쓰지 않아도 된다.

저장

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

회원1과 상품A의 연관관계를 설정했으므로 회원1을 저장할 때 연결 테이블에도 값이 저장된다.

따라서 이 코드를 실행하면 다음과 같은 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 p : products)
    {
        System.out.println("product.name = " + p.getName());
    }
}

member.getProducts()를 호출해서 상품 이름을 출력하면 다음 SQL이 실행된다.

SELECT * FROM MEMBER_PRODUCT MP
INNER JOIN PRODUCT P ON MP.PRODUCT_ID=P.PRODUCT_ID
WHERE MP.MEMBER_ID=?

실행된 SQL을 보면 연결 테이블인 MEMBER_PRODUCT와 상품 테이블을 주인해서 연관된 상품을 조회한다.

 

@ManyToMany 덕분에 복잡한 다대다 관계를 애플리케이션에서는 아주 단순하게 사용할 수 있다.

02. 다대다 : 양방향

더보기
@Entity
public class Product
{
    @Id
    private String id;
    
    @ManyToMany(mappedBy = "products") // 역방향 추가
    private List<Member> members;
    ...
}

위 코드를 보면 다대다 매핑이므로 역방향도 @ManyToMany를 사용한다.

그리고 양쪽 중 원하는 곳에 mappedBy로 연관관계의 주인을 지정한다(물론 mappedBy가 없는 곳이 연관관계의 주인이다.)

member.getProducts().add(product);
product.getMembers().add(member);

다대다의 양방향 연관관계는 다음처럼 설정하면 된다.

 

public void addProduct(Product product)
{
    ...
    products.add(product);
    product.getMembers().add(this);

양방향 연관관계는 연관관계 편의 메소드를 추가해서 관리하는 것이 편리하다.

위 코드처럼 회원 엔티티에 연관관계 편의 메소드를 추가했다.

 

연관관계 편의 메소드를 추가했으므로 .addProduct() 메소드로 간단히 연관관계를 설정할 수 있게 되었다.

public void findInverse()
{
    Product product = em.find(Product.class, "productA");
    List<Member> members = product.getMembers();
    for (Member m : members)
    {
        System.out.println("member = " + member.getUsername());
    }
}

양방향 연관관계로 만들었으므로 product.getMembers()를 사용해서 역방향으로 객체 그래프를 탐색할 수 있다.

03. 다대다 : 매핑의 한계와 극복, 연결 엔티티 사용

더보기

@ManyToMany를 사용하면 연결 테이블을 자동으로 처리해주므로 도메인 모델이 단순해지고 여러 가지로 편리하다.

하지만 이 매핑을 실무에서 사용하기에는 한계가 있다.

자바 ORM 표준 JPA 프로그래밍

예를 들어 회원이 상품을 주문하면 연결 테이블에 단순히 주문한 회원 아이디와 상품 아이디만 담고 끝나지 않는다.

보통은 연결 테이블에 주문 수량 컬럼이나 주문한 날짜 같은 컬럼이 더 필요하다.

 

위 그림을 보면 연결 테이블에 주문 수량(ORDERAMOUNT) 과 주문 날짜(ORDERDATE) 컬럼을 추가했다.

이렇게 컬럼을 추가하면 더는 @ManyToMany를 사용할 수 없다.

왜냐하면 주문 엔티티나 상품 엔티티에는 추가한 컬럼들을 매핑할 수 없기 때문이다.

 

자바 ORM 표준 JPA 프로그래밍

결국 위 그림처럼 연결 테이블을 매핑하는 연결 엔티티를 만들고 이곳에 추가한 컬럼들을 매핑해야 한다.

그리고 엔티티 간의 관계도 테이블 관계처럼 다대다에서 일대다, 다대일 관계로 풀어야 한다.

여기서는 회원상품(MemberProduct)엔티티를 추가했다.

@Entity
public class Member
{
    @Id
    @Column(name = "MEMBER_ID")
    private String id;
    
    // 역방향
    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProducts;
    
    ...
}

연결 테이블에 주문 수량(ORDERAMOUNT)과 주문 날짜(ORDERDATE) 컬럼을 추가했고 나머지 테이블은 기존과 같다.

 

회원과 회원상품을 양방향 관계로 만들었다.

회원상품 엔티티 쪽이 외래키를 가지고 있으므로 연관관계의 주인이다.

따라서 연관관계의 주인이 아닌 회원의 Member.memberProducts에는 mappedBy를 사용했다.

@Entity
public class Product
{
    @Id
    @Column(name = "PRODUCT_ID")
    private String id;
    
    private String name;
    
    ...
}

위 코드를 보면 상품 엔티티에서 회원상품 엔티티로 객체 그래프 탐색 기능이 필요하지 않다고 판단해서 연관관계를 만들지 않았다.

 

회상품 엔티티 코드

@Entity
@IdClass(MemberProdudctId.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() {...}
}

회원상품(MemberProduct)엔티티를 보면 기본 키를 매핑하는 @Id와 외래 키를 매핑하는 @JoinColumn을 동시에 사용해서 기본 키 + 외래 키를 한번에 매핑했다.

그리고 @IdClass를 사용해서 복합 기본 키를 매핑했다.

 

  • 복합 기본 키

회원상품 엔티티는 기본 키가 MEMBER_ID와 PRODUCT_ID로 이루어진 복합 기본지(간단히 복합키)다.

JPA에서 복합 키를 사용하려면 별도의 식별자 클래스를 만들어야 한다.

그리고 엔티티에 @IdClass를 사용해서 식별자 클래스를 지정하면 된다.

여기서는 MemberProductId 클래스를 복합 키를 위한 식별자 클래스로 사용한다.

 

복합키를 위한 식별자 클래스는 다음과 같은 특징이 있다.

  • 복합 키별도의 식별자 클래스로 만들어야 한다.
  • Serializable을 구현해야 한다.
  • equals와 hashCode 메소드를 구현해야 한다.
  • 기본생성자가 있어야 한다.
  • 식별자 클래스는 public 이어야 한다.
  • @IdClass를 사용하는 방법 외에 @EmbeddedId를 사용하는 방법도 있다.

 

  • 식별 관계

회원상품은 회원과 상품의 기본 키를 받아서 자신의 기본 키로 사용한다.

이렇게 부모 테이블의 기본 키를 받아서 자신의 기본 키 + 외래 키로 사용하는 것을 DB용어로 식별관계라고 한다.

 

종합해보면

회원상품(MemberProduct)은 회원의 기본 키를 받아서 자신의 기본 키로 사용함과 동시에 회원과의 관계를 위한 외래 키로 사용한다.

그리고 상품의 기본 키도 받아서 자신의 기본 키로 사용함과 동시에 상품과의 관계를 위한 외래 키로 사용한다.

또한 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);
}

위 코드로 회원상품 엔티티를 만들면서 연관된 회원 엔티티와 상품 엔티티를 설정했다.

회원상품 엔티티는 DB에 저장될 때 연관된 회원의 식별자와 상품의 식별자를 가져와서 자신의 기본 키 값으로 사용한다.

 

public void find()
{
    // 기본 키 값 생성
    MemberPRoductId memberProductId = new MemberProductId();
    memberProductId.setMember("member1");
    memberPRoductId.setProduct("productA");
    
    MemberProduct memberProduct = em.find(MemeberProduct.class, memberProductId);
    
    Member member = memberProduct.getMember();
    Product product = memberProduct.getProduct();
    
    System.out.println("member = " + member.getUsername());
    System.out.println("product = " + product.getName());
    System.out.println("orderAmount = " + memberProduct.getOrderAmount());
    
}

지금까지는 기본 키가 단순해서 기본 키를 위한 객체를 사용하는 일이 없었지만 복합 키가 되면 이야기가 달라진다.

복합 키는 항상 식별자 클래스를 만들어야 한다.

em.find()를 보면 생성한 식별자 클래스로 엔티티를 조회한다.

 

복합 키를 사용하는 방법은 복잡하다. 

단순히 컬럼 하나만 기본 키로 사용하는 것과 비교해서 복합 키를 사용하면 ORM 매핑에서 처리할 일이 상당히 많아진다.

복합 키를 위한 식별자 클래스도 만들어야 하고 @IdClass 또는 @EmbeddedId도 사용해야 한다.

그리고 식별자 클래스에 equals, hashCode도 구현해야 한다.

 

04. 다대다 : 새로운 기본 키 사용

더보기

추천하는 기본 키 생성 전략은 DB에서 자동으로 생성해주는 대리 키를 Long값으로 사용하는 것이다.

이것의 장점은 간편하고 거의 영구히 쓸 수 있으며 비즈니스에 의존하지 않는다.

그리고 ORM 매핑 시에 복합 키를 만들지 않아도 되므로 간단히 매핑을 완성할 수 있다.

 

자바 ORM 표준 JPA 프로그래밍

이번에는 연결 테이블에 새로운 기본 키를 사용할 것이다.

이 정도 되면 회원상품(MemberProduct)보다는 주문(Order)이라는 이름이 더 어울릴 것이다.

 

ORDER_ID라는 새로운 기본 키를 하나 만들고 MEMBER_ID, PRODUCT_ID 컬럼은 외래 키로만 사용한다.

@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;
    ...
}

대리 키를 사용함으로써 이전에 보았던 식별 관계에 복합 키를 사용하는 것보다 매핑이 단순하고 이해하기 쉽다.

 

다음은 저장하고 조회하는 코드이다.

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);
    
    // 주문 저장
    Order order = new Order();
    order.setMember(member1);	// 주문 회원 - 연관관계 설정
    order.setProduct(productA); // 주문 회원 - 연관관계 설정
    order.setOrderAmount(2); 	// 주문 수량
    em.persist(order);
}
728x90

'JPA' 카테고리의 다른 글

JPA TIL - 09 값 타입  (0) 2024.05.15
JPA TIL - 07 고급 매핑  (0) 2024.05.08
JPA TIL - 05 연관관계 매핑 기초  (0) 2024.04.09
JPA TIL - 04 엔티티 매핑  (0) 2024.04.02
JPA TIL - 03 영속성 관리  (0) 2024.03.26
728x90

운영체제의 개요

 

1. 운영체제 구조와 기능

더보기

운영체제란

컴퓨터의 효율적인 운영을 담당하는 시스템 소프트웨어

  • 시스템의 자원인 CPU, RAM, HDD, I/O장치, 네트워크 등을 효율적으로 관리하고 운영
  • 사용자와 컴퓨터 하드웨어 간의 인터페이스 역할을 함

운영체제의 목표

  1. 컴퓨터 내의 하드웨어/소프트웨어 자원을 관리함
  2. 컴퓨터에 사용자가 쉽게 접근할 수 있는 인터페이스를 제공함
  3. 수행 중인 프로그램들의 효율적인 운영을 도움
  4. 작업 처리 과정 중의 데이터를 공유함
  5. 입출력에 보조적인 기능을 수행함
  6. 오류가 발행하면 오류를 처리함

운영체제의 기능

  • 프로세스 관리 : 프로세스의 생성, 삭제, 동기화 등에 관여
  • 작업 관리 : 프로세스가 수행하는 작업에 대한 관리
  • 주기억 장치 관리 : 주기억 장치의 할당과 회수를 관리
  • 보조기억 장치 관리 : 보조기억 장치의 사용을 관리
  • 입출력 장치 관리 : 입출력 장치를 관리함
  • 파일 관리 : 파일 생성·빈 공간 관리·디스크 스케줄링 등을 담당
  • 보안 관리사용자 계정 관리 및 불법 침입 등 보안 관련 사항 관리

 

2. 운영체제 처리유형

더보기

일괄처리(Batch Processing)

  • 여러 사용자들의 작업들을 모아서 일괄적으로 순서대로 처리하는 방식
  • 실행 요청 순서에 따라 순차적으로 프로그램을 실행

다중프로그래밍(Multi-programming)

  • 한 프로그램이 입출력 등의 작업을 할 때 다른 프로그램을 수행
  • 중앙처리장치를 쉬게 하지 않고 처리하게 하여 전체적인 처리 속도를 향상시키는 방식

시분할 시스템(Time-sharing System)

  • CPU 운영 시간을 잘게 쪼개어 여러 사용자에게 골고루 처리 시간을 제공
  • 특정 사용자가 오래 기다리는 것을 방지

다중처리(Multi-Processing)

  • 두 개 이상의 프로세서로 구성된 시스템의 운영체제로서, 단일 처리 시스템보다 많은 양의 작업을 동시에 처리할 수 있는 방식

 

3. 운영체제 발전 과정

더보기

유닉스(Unix)

  • 고가의 컴퓨터를 여러 사람이 이용할 수 있는 운영체제 개발을 시도
  • 1960년대 초 : MIT, Bell Lab, GE 등이 공동으로 MULTICS 개발 시작
  • 1969년 AT&T 연구원 Ken Tompson과 Dennis Ritchie가 UNICS 개발
  • 1973년 Dennis Ritchie가 C언어로 다시 개발 -> 고급 언어로 작성된 최초의 운영체제
  • 1976년 미국의 대학과 기업에 확산
  • 1978년 버클리 대학의 Bill Joy와 Chuck Haley가 BSD* UNIX 발표
  • 1983년 AT&T가 System V 발표
  • 특징
    • 전문 프로그래머용으로 개발 다양한 프로그램 제작
    • 고급 언어로 작성되어 다양한 하드웨어에 설치 가능
    • 연구용으로 소스 공개 대학이나 연구소에서 많이 사용
    • 다중 사용자 시스템
    • 강력한 네트워크 기능 지원
    • X Windows 도구로 GUI 환경 제공

리눅스(Linux)

  • 1991년 핀란드의 헬싱키 대학생이던 Linus Torvalds가 개발
  • GNU 프로젝트에 참여하면서 급속하게 성장
  • 무료이며 소스 코드가 완전히 공개된 운영체제
  • 누구나 다운받아 설치할 수 있으며, 다수의 배포판이 존재

 

Mac OS

  • 애플에서 개발한 GUI 기반의 운영체제
  • FreeBSD를 변형하여 매킨토시 컴퓨터에서 동작
  • 1984년~2001년 : 클래식 Mac OS (버전 1 ~ 9)
  • 2001년~2015년 : Mac OS X
  • 2016년 ~ 현재 : macOS
    • 아이맥, 맥북프로 출시 (인텔기만 매킨토시) 
    • iOS, watchOS, tvOS 와 통일된 명칭 사용

Window OS

  • 마이크로소프트사에서 만든 그래픽 사용자 인터페이스(GUI)기반 운영체제
  • 1985년 Windows 1.0
  • 1992년 Windows 3.1 : MS-DOS에 GUI를 추가한 구조
  • 1995년 Windows 95 : 자체적 GUI 환경의 OS
  • 2001년 Windows XP : 윈도우 NT 기반으로 전면 개선
  • 2009년 Windows 7
  • 2012년 Windows 8
  • 2015년 Windows 10
  • 2021년 Windows 11

프로세스 관리와 스케줄링

 

1. 프로세스 관리

 

더보기

프로세스(Process)

실행을 위해서 주기억장치에 올려진 프로그램 (운영체제 관리)

프로그램 코드 뿐만 아니라 실행에 필요한 다양한 정보도 포함

운영체제에서 실행되는 시스템 작업의 기본 단위

 

프로세스 생명 주기

준비 : 프로세스 실행 기다림

실행 : CPU에서 프로세스 실행, CPU는 한순간에 하나의 프로세스만 실행

대기 : 어떤 사건이 일어나기를 기다림

 

 

프로세스 제어 블록(Process Control Block)

  • 운영체제는 프로세스 각각에 대한 정보 관리를 통해 프로세스를 제어함
  • 프로세스에 대한 정보는 PCB(Process Control Block)에 저장됨
  • 임의의 프로세스가 생성되면 PCB가 생성되며 프로세스 종료 시 없어짐
  • PCB 정보
    • 프로세스의 상태 : 실행 상태인지 또는 실행을 위해 준비상태인지 등의 정보
    • PC 값 : 다음에 실행될 명령어의 주기억장치 주소를 의미, PC 레지스터에 저장
    • 스케줄링 정보 : 다음에 실행될 프로세스를 결정하는 데 필요한 정보로, 프로세스 스케줄링 정책, 우선순위등을 의미
    • 주기억장치 정보 : 해당 프로세스가 주기억장치 어느 영역에 위치해있는지를 저장 

프로세스 생성

 

  • 생성된 프로세스 A의 프로세스 제어 블록이 준비 큐에 연결 - 준비 상태
  • 중앙처리장치가 프로세스 A를 실행 - 실행 상태

프로세스 상태 - 준비, 실행

  • 프로세스 B와 C가 생성, 준비 상태
  • 중앙처리장치가 프로세스 A를 실행 – 실행 상태

프로세스 상태 - 대기

  • 프로세스 A에 디스크 입출력 명령이나 sleep 명령이 발생할 경우 : 대기 상태
  • 기다리던 이벤트가 종료되면 준비 큐로 이동해서 준비 상태가 됨

프로세스 종료

  • 프로세스 A의 실행이 종료된 경우 : PCB와 프로세스가 메모리에서 삭제됨

2. 프로세스 스케줄링

더보기

다중 프로그래밍(Multi-programming)

  • 여러 개의 프로세스를 주기억장치에 적재
  • 실행 중인 프로세스가 대기하는 경우 다른 프로세스가 CPU에서 실행
  • CPU 이용률을 최대화하는 개념

 

프로세스 스케줄링(Process Scheduling)

운영체제가 어떤 프로세스를 실행할 지를 결정하는 것

  • FCFS(First-Come First-Served) 스케줄링 : 먼저 도착한 프로세스를 먼저 서비스(실행)하는 방법
  • 라운드 로빈(Round Robin) 스케줄링
  • Shortest-Job-First(SJF)

라운드 로빈(Round Robin) 스케줄링

  • 여러 프로세스들이 CPU를 돌아가며 일정한 시간을 할당받아 실행되는 방식
  • 프로세스들은 시간 할당량(time quantum) 동안 CPU를 할당받아 실행

Shortest-Job-First(SJF)

  • 각 프로세스는 자신의 수행 시간을 제시
  • 수행 시간이 가장 작은 프로세스에 먼저 CPU를 할당
  • 작업들의 평균 대기시간이 최소가 됨
  • 단점 : 무한봉쇄(Indefinite blocking), 기아상태(Starvation)

우선순위(Priority) 스케줄링

  • 가장 높은 우선순위의 프로세스에게 먼저 CPU를 할당
  • 우선 순위가 같은 프로세스는 FCFS 방법을 적용
  • 단점 : 무한봉쇄(Indefinite blocking), 기아상태(Starvation)
  • 에이징(Aging) 기법 : 기다린 시간에 비례하여 우선 순위를 높여줌

스케줄링 평가 기준

  1. 공정성 (특정 프로세스가 불이익을 당하는가) 
  2. Throughput (단위시간에 끝나는 작업 수) 
  3. Response time 
  4. 요청 목적에 맞는가(급한 작업, 여유 있는 작업) 
  5. 예측 가능성 (요청 시마다 시간 편차가 큰지 여부) 
  6. 오버 헤드 최소화 (스케줄링에 소요되는 시간과 공간) 
  7. 균형적인 자원 이용 (다른 자원의 활용도를 높이는가)

주기억장치와 파일 관리

1. 주기억장치 관리

 

더보기

주기억장치(Main Memory)

  • 현재 실행 중인 프로그램과 이 프로그램이 필요로 하는 데이터를 일시적으로 저장하는 장치
  • 운영체제는 현재 사용되고 있는 주기억장치 영역과 사용되지 않는 영역에 대한 정보를 유지
  • 프로세스에게 메모리를 할당하고, 프로세스가 종료되면 회수

단일 프로그래밍

  • 주기억장치에 운영체제 외 한 개의 사용자 프로그램만 저장하는 관리 기법
  • 주기억장치를 두 영역으로 나누어 저장

다중 프로그래밍 - 분할 메모리 관리

  • 주기억장치(메모리)를 n개의 영역으로 분할하여 서로 다른 프로세스를 동시에 저장

분할 메모리 관리 할당 방식

  • 새롭게 생성된 프로세스를 메모리에 할당하는 방식
  • 최초 적합 : 프로세스의 크기보다 큰 최초의 영역에 할당
  • 최적 적합 : 프로세스의 크기보자 큰 영역 중 가장 작은 영역에 할당
  • 최악 적합 : 프로세스의 크기보다 큰 영역 중 가장 큰 영역에 할당

가상 메모리(Virtual Memory)

  • 실행될 프로그램이 주기억장치보다 크거나 여러 개인 경우에는 주기억장치의 공간 부족으로 인해 프로그램 실행에 제약이 됨
  • 당장 실행에 필요한 부분만 주기억장치에 저장하고, 당장 필요하지 않는 나머지 부분은 보조기억장치에 넣어두고 실행

페이지(Page)

  • 프로그램을 일정한 크기로 나눈 단위 
  • 페이지 단위로 주기억장치에 프로그램이 올라가서 동작

페이징(Paging)

  • 가상메모리를 구현하는 한 방법
  • 페이지 프레임 : 실제 주기억장치의 페이지가 저장되는 고정된 크기의 블록

페이지 테이블 (Page Table)

  • 프로세스마다 각 페이지가 주기억장치의 어느 프레임에 저장되는지를 나타내는 테이블

 

페이지 교체

새로운 페이지를 주기억장치에 저장할 때 비어있는 프레임이 없으면?

-> 새로운 페이지를 저장하기 위해 주기억장치에서 제거할 페이지를 결정하고 교체함

페이지 교체 알고리즘

  • FIFO (First-In First-Out) 알고리즘
    • 페이지를 교체해야 할 때 주기억장치에 가장 먼저 올라온 페이지를 선택해서 제거하는 기법
  • LRU (Least Recently Used) 알고리즘
    • 페이지를 교체해야 할 때, 주기억장치에 올라온 페이지들 중에서 가장 오랫동안 사용되지 않았던 페이지를 선택해서 제거하는 기법
  • LFU(Least Frequently Used)알고리즘
    • 페이지를 교체해야 할 때 페이지들 중에서 사용 빈도가 가장 낮은 페이지를 선택해서 제거하는 기법

 

2. 파일 관리

 

더보기

파일 시스템

  • 파일은 보조기억장치에 저장된 연관성 있는 정보(데이터)의 집합
  • 프로세스가 파일을 생성할 때 이름을 부여
  • 파일을 읽고 쓰는 동작은 운영체제가 담당

FAT(File Allocation Table)

  • 트랙(Track)과 섹터(Sector)로 구성
  • 간단한 구조로 PC 운영체제 대부분이 지원 - 파일 공유에 적합
  • 메모리 카드나 USB 메모리 등에서 사용
  • 디스크 오류 등 발생 시 안정성이 떨어짐
  • FAT, FAT16, FAT32, VFAT 등 형식이 존재
  • exFAT는 파일 크기 한계를 극복 - 최대 512TB까지 저장

NTFS(New Technology File System)

  • FAT을 대체하기 위해 개발
  • 메타데이터 지원, 성능 개선, 신뢰성 향상 등
  • 파일 용량 한계 확장
    • FAT32 : 파일 최대 4GB, 드라이브 최대 32GB
    • NTFS : 파일 최대 16TB, 드라이브 256TB
  • 보안성, 안정성이 뛰어나지만 호환성이 부족

HFS(Hierarchical Technology File System)

  • 애플이 개발한 파일시스템
  • Mac OS에서 사용
    • 파일개수 최대 65,536개
    • 파일 용량 최대 2GB
  • 1998년 HFS+ 발표
  • 2016년 APFS(Apple File System)으로 전환

 

728x90

+ Recent posts