[자바 ORM 표준 JPA 프로그래밍] 11주차 스터디

박서영·2026년 6월 27일

11장. 웹 애플리케이션 제작

예제에서 사용하는 기술

  • 뷰: JSP, JSTL
  • 웹 계층: 스프링 MVC
  • 데이터 저장 계층: JPA, 하이버네이트
  • 기반 프레임워크: 스프링 프레임워크
  • 빌드: 메이븐

✔️ 11.1 프로젝트 환경설정

프로젝트 환경설정 진행순서

  1. 프로젝트 구조 분석
  2. 메이븐(Maven)과 라이브러리 설정
  3. 스프링 프레임워크 설정
  • 프로젝트 폴더 구조를 분석 → 메이븐에 사용할 라이브러리 지정 → 스프링 프레임워크 환경 설정
  • 웹 서버 실행: 톰캣 플러그인 사용
    • 메뉴의 [Run] > [Run As] > [Maven Build…] 선택

    • Goals에 tomcat7:run을 입력하고 Run 버튼 선택

      http://localhost:8080으로 서버가 실행됨

(1) 프로젝트 구조

메이븐이 제공하는 표준 프로젝트 구조

jpashop (프로젝트 루트)
├── src (소스 폴더)
|    ├── main(실행 코드)
|    |     ├── java (자바 소스코드)
|    |     ├── resources (리소스)
|    |     └── webapp (웹 폴더)
|    └── test (테스트 코드)
├── target (빌드 결과)
└── pom.xml (메이븐 설정 파일)

(2) 메이븐과 사용 라이브러리 관리

pom.xml 파일

  • : POM 모델 버전.
  • : 프로젝트 그룹명 ex. org.springframework
  • : 프로젝트를 식별하는 아이디 ex. spring-mvc
  • : 프로젝트 버전
  • : 프로젝트 이름
  • : 빌드 패키징 방법을 지정함. 웹 애플리케이션은 war, 자바 라이브러리는 jar로 설정
  • : 사용할 라이브러리를 지정
  • : 빌드 관련 정보를 설정

dependencies 파일

  • 핵심 라이브러리
    • 스프링 MVC: 스프링 MVC 라이브러리
    • 스프링 ORM: 스프링 프레임워크와 JPA를 연동하기 위한 라이브러리
    • JPA, 하이버네이트: JPA 표준과 하이버네이트를 포함하는 라이브러리. hibernate-entitymanager를 라이브러리로 지정하면 다음 중요 라이브러리도 함께 내려받음
      • hibernate-core-(버전정보).jar: 하이버네이트 라이브러리
      • hibernate-jpa-2.1-api-(버전정보).jar: JPA 2.1 표준 인터페이스가 있는 라이브러리
  • 기타 라이브러리
    • H2 데이터베이스: H2 데이터베이스는 아주 작은 데이터베이스로 별도 설치 없이 JVM 메모리 안에서 동작하는 기능도 있어 실습으로 적당함.
    • WEB: 서블릿, JSP 관련 라이브러리
    • 로깅 SLF4J & LogBack: 과거 Log4j가 많이 사용됐지만, 최근 SLF4J & LogBack이 많이 사용함.
    • 테스트: 테스트용 라이브러리. spring-test는 스프링 프레임워크와 통합 테스트를 지원

테스트

  • target 폴더에 빌드 결과로 jpashop.war 파일이 생성됨

(3) 스프링 프레임워크 설정

web.xml: 웹 애플리케이션 환경설정 파일

  • 스프링 프레임워크를 구동하기 위한 설정
    • webAppConfig.xml: 스프링 MVC 설정을 포함해서 웹 계층을 담당
    • appConfig.xml: 비즈니스 로직, 도메인 계층, 서비스 계층, 데이터 저장 계층을 담당

webAppConfig.xml: 스프링 웹 관련 환경설정 파일

  • <mvc:annotation-driven>: 스프링 MVC 기능을 활성화
  • <context: component-scan>: basePackages를 포함한 하위 패키지를 검색해 @Component, @Service, @Repository, @Controller 어노테이션이 붙어있는 클래스들을 스프링 빈으로 자동 등록함.
  • : 스프링 빈을 등록

appConfig.xml: 스프링 애플리케이션 관련 환경설정 파일

  • <tx: annotation-driven/>: 스프링 프레임워크가 제공하는 어노테이션 기반의 트랜잭션 관리자를 활성화함. 이 기능은 @Transactional이 붙은 곳에 트랜잭션을 적용함
jpashop (프로젝트 루트)
├── src (소스 폴더)
|    ├── main(실행 코드)
|    |     ├── java (자바 소스코드)
|		 |     |     └── jpabook
|    |     |             └── jpashop
|    |     |                     ├── domain //도메인 계층
|    |     |                     ├── repository //데이터 저장 계층
|    |     |                     ├── service //서비스 계층
|    |     |                     └── web //웹 계층
|    |     |
|    |     ├── resources (리소스)
|    |     |       └── **appConfig.xml** //스프링 애플리케이션 관련 설정
|    |     |       └── **webAppConfig.xml** //스프링 웹 관련 설정
|    |     └── webapp (웹 폴더)
|    |            └── WEB-INF
|    |                   └── **web.xml** //웹 애플리케이션 환경설정 파일
|    └── test (테스트 코드)
├── target (빌드 결과)
└── pom.xml (메이븐 설정 파일)

데이터소스 설정

<bean id="dataSource" class="org.apache.tomcat.jdbc.pool.DataSource">
	<property name="driverClassName" value="org.h2.Driver"/>
	<property name="url" value="jdbc:h2:mem:jpashop"/>
	<property name="username" value="sa"/>
	<property name="password" value=""/>
</bean>
  • 데이터베이스에 접근할 데이터소스를 등록
  • H2 데이터베이스의 접속 URL을 jdbc:h2:mem:...으로 설정해 JVM 안에서 동작하는 인 메모리 데이터베이스로 사용함
    • 인 메모리 데이터베이스 → 별도의 데이터베이스 서버를 실행하지 않아도 됨
  • 애플리케이션을 시작할 때 데이터베이스도 애플리케이션 안에서 함께 실행되고 애플리케이션을 종료할 때 함께 사라짐

트랜잭션 관리자 설정

<bean id="transactionalManager" class="org.springframework.orm.jpa.JpatransactionManager">
	<property name="dataSource" ref="dataSource">
</bean>
  • 트랜잭션 관리자를 등록
  • JPA 사용을 위해서는 org.springframework.orm.jpa.JpatransactionManager를 트랜잭션 관리자로 등록해양함

JPA 예외 변환 AOP 설정

<bean class="org.springframework.dao.annoation.PersistenceExceptionTranslationPostProcessor"/>
  • @Repository 어노테이션이 붙어있는 스프링 빈에 예외 변환 AOP를 적용함

JPA 설정-엔티티 매니저 팩토리 등록

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
	<property name="dataSource" ref="dataSource">
	<!--@Entity 탐색 시작 위치-->
	<property name="packagesToScan" value="jpabook.jpashop.domain"/>
	<property name="jpaVendorAdapter">
		<!--하이버네이트 구현체 사용-->
		<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
	</property>
	<property name="jpaProperties"> <!--하이버네이트 상세 설정-->
		<props>
			<prop key="hibernate.dialect">
				org.hibernate.dialect.H2Dialect </prop> <!--방언-->
			<prop key="hibernate.show_sql">true</prop> <!--SQL 보기-->
			<prop key="hibernate.format_sql">true</prop> <!--SQL 정렬해서 보기-->
			...
			
  • 스프링 프레임워크에서 JPA를 사용하기 위해서는 LocalContainerEntityManagerFactoryBean을 스프링 빈으로 등록해야함
  • J2SE 환경(순수 자바만 사용)에서는 persistence.xml에 엔티티 매니저 팩토리 정보를 설정
    • LocalContainerEntityManagerFactoryBean: JPA 스프링 컨테이너에서 사용할 수 있도록 스프링 프레임워크가 제공하는 기능. 해당 클래스는 spring-orm 라이브러리가 제공
    • dataSource: 사용할 데이터소스를 등록
    • packagesToScan: @Entity가 붙은 클래스를 자동으로 검색하기 위한 시작점을 지정
    • persistenceUnitName: 영속성 유닛 이름을 지정. 지정하지 않을 시, default라는 이름의 영속성 유닛을 생성
    • jpaVendorAdapter: 사용할 JPA 벤더를 지정.

하이버네이트 속성 설정

  • hibernate.dialect: 사용할 데이터베이스 방언을 설정
  • hibernate.show_sql: 실행하는 SQL을 콘솔에 출력
  • hibernate.format_sql: SQL을 보기 좋게 정리해서 출력
  • hibernate.use_sql_comments: SQL 출력 시 어떻게 실행된 SQL인지 또는 사용자가 설정한 코멘트를 남김
  • hibernate.id.new_generator_mapping: JPA에 맞춘 새로운 ID 생성 방법을 사용.
  • hibernate.hbm2ddl.auto: 애플리케이션 시작 시, 테이블과 기타 DDL을 자동으로 생성 (4가지 옵션 존재)
    • create: 기존 DDL을 제거하고 새로 생성
    • create-drop: create와 같지만, 애플리케이션 종료 시 생성했던 DDL을 삭제함
    • update: 현재 데이터베이스 DDL과 비교해 변경사항만 수정
    • validate: 현재 엔티티 매핑 정보와 데이터베이스 스키마가 같은지 비교. 다를 경우, 경고를 남기고 애플리케이션 실행하지 않음. DDL을 변경하지 않는 옵션.

✔️ 11.2 도메인 모델과 테이블 설계

(1) 요구사항 분석

  • 회원기능: 회원 등록, 회원 조회
  • 상품기능: 상품 등록, 상품 수정, 상품 조회
  • 주문기능: 상품 주문, 주문 내역 조회, 주문 취소
  • 기타 요구사항: 상품 종류에는 ‘도서, 음반, 영화’ 존재. 상품을 카테고리로 구분 가능, 상품 주문 시 배송 정보 입력 가능

(2) 도메인 모델 설계

  • 회원-주문-상품의 관계:
    • 회원은 여러 상품 주문 가능
    • 한 번의 주문에 여러 상품을 선택 가능 ⇒ 주문-상품은 다대다 관계
      • 이런 다대다 관계는 관계형 데이터베이스 및 엔티티에서도 거의 사용하지 않기 때문에, 주문 상품이라는 엔티티를 추가해 다대다 관계를 일대다, 다대일 관계로 품.
  • 상품 분류: 상품은 도서, 음반, 영화로 구분되는데 이대 상품의 공통 속성을 사용하기에 상속 구조로 표현함

  • 회원:
    • 이름, 주문한 상품들, 주소(임베디드 타입)
  • 주문: 주문-주문상품은 일대다 관계.
    • 주문한 회원, 배송정보, 주문날짜, 주문 상태(열거형)
  • 주문상품
    • 주문한 상품 정보, 주문 금액, 주문 수량 정보
  • 상품
    • 이름, 가격, 재고수량
  • 배송: 주문 시 하나의 배송 정보를 생성. 주문-배송은 일대일 관계
  • 카테고리: 카테고리-상품 다대다 관계
  • 주소: 값 타입(임베디드 타입). 회원과 배송에서 사용

(3) 테이블 설계

  • MEMBER: 회원 엔티티의 주소 임베디드 타입 정보가 회원 테이블에 그대로 들어감
  • ITEM: 앨범, 도서, 영화 타입을 통해해 하나의 테이블로 만들고 DTYPE 컬럼을 통해 구분.

(4) 연관관계 정리

  • 회원과 주문: 일대다 양방향 관계.
    • 외래키가 있는 주문이 연관관계의 주인
    • Order.member를 ORDERS.MEMBER_ID 외캐리와 매핑함
  • 주문상품과 주문: 다대일 양방향 관계
    • 주문상품이 연관관계의 주인.
    • OrderItem.order를 ORDER_ITEM.ORDER_ID 외래키와 매핑함
  • 주문과 배송: 일대일 양방향 관계
    • Order.delivery를 ORDERS.DELIVERY_ID 외래키와 매핑함
  • 카테고리와 상품: @ManyToMany를 사용해 매핑함

(5) 엔티티 클래스

  • 앞의 설계를 기반으로 필요한 엔티티 클래스의 코드를 작성함
  • 아래는 Member의 클래스
...
@Entity
public class Member {

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

    private String name;

    @Embedded
    private Address address;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<Order>();

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    public List<Order> getOrders() {
        return orders;
    }

    public void setOrders(List<Order> orders) {
        this.orders = orders;
    }

    @Override
    public String toString() {
        return "Member{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", address=" + address +
                '}';
    }
}

11.3 애플리케이션 구현

(1) 개발 방법

!image.png

  • Controller: MVC의 컨트롤러가 모여있는 곳. 서비스 계층을 호출하고 결과를 뷰(JSP)에 전달
  • Service: 서비스계층에는 비즈니스 로직이 있고 트랜잭션을 시작함. 서비스 계층은 데이터 접근 계층인 레포지토리를 호출함.
  • Repository: JPA를 직접 사용하는 곳. 엔티티를 매니저를 사용해 엔티티를 저장하고 조회.
  • Domain: 엔티티가 모여있는 계층으로 모든 계층에서 사용.

⇒ 개발순서: 비즈니스 로직을 수행하는 서비스(Service)와 레포지토리(Repository) 계층을 먼저 개발 → 테스트 케이스를 작성해 검증 → 검증 완료 후 컨트롤러와 뷰를 개발

(2) 회원 기능

  • 구현기능: 회원 등록, 회원 목록 조회

도메인(Domain)

@Entity
public class Member {
	
	@Id @GeneratedValue
	@Column(name = "MEMBER_ID")
	private Long id;
	
	private String name;
	
	@Embedded
	private Address address;
	
	@OneToMany(mappedBy = "member")
	private List<Order> orders = new ArrayList<Order>();
	
	...
}

레포지토리(Repository)

  • @Repository:
    • <context:component-scan>에 의해 스프링 빈으로 자동 등록됨
    • JPA 전용 예외 발생 시, 스프링이 추상화한 예외로 변환해줌
  • @PersistenceContext:
    • 컨테이너가 관리하는 엔티티 매니저를 주입하는 어노테이션
    • 순수 자바 환경에서 엔티티 매니저 팩토리에서 엔티티 매니저를 직접 생성해 사용하지만, 스프링/J2EE 컨테이너 사용 시, 컨테이너가 엔티티 매니저를 관리하고 제공함. 따라서, 엔티티 매니저를 직접 생성해 사용하는 것이 아니라, 컨테이너가 제공하는 엔티티 매니저를 사용해야함
  • @PersistenceUnit
    • 엔티티 매니저 팩토리를 주입받기 위해서 사용
@Repository
public class MemberRepository {

	@PersistenceContext
	EntityManager em;
	
	public void save(Member member) {
		em.persist(member);
	}
	
	public Member findOne(Long id) {
		return em.find(Member.class, id);
	}
	
	public List<Member> findAll() {
		return em.createQuery("select m from Member m", Member.class).getResultList();
	}
	
	public List<Member> findByName(String name) {
		return em.createQuery("select m from Member m where m.name = :name", Member.class)
		.setParameter("name", name)
		.getResultList();
	}
}

서비스(Service)

  • @Service: <context:component-scan>에 의해 스프링 빈으로 등록됨
  • @Transactional: 트랜잭션을 적용해줌
    • 외부에서 해당 클래스의 메소드를 호출하면 트랜잭션을 시작하고, 메소드가 종료될 때, 트랜잭션을 커밋함. 예외 발생 시, 트랜잭션을 롤백함
  • @Autowired: 스프링 컨테이너가 적절한 스프링 빈을 주입해줌.
@Service
@Transactional
public class MemberService {

    @Autowired
    MemberRepository memberRepository;

    public Long join(Member member) {

        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }
		
    private void validateDuplicateMember(Member member) {
        List<Member> findMembers = memberRepository.findByName(member.getName());
        if (!findMembers.isEmpty()) {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        }
    }

    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    public Member findOne(Long memberId) {
        return memberRepository.findOne(memberId);
    }
}
  • 회원가입 (join())
    • validateDuplicateMember()를 통해 중복 회원가입을 방지함 동일한 이름을 가진 회원이 존재한다면, 메시지와 함께 예외를 발생시키고 성공했을 경우에는 회원 식별자를 반환해줌

테스트

  • @RunWith(SpringJUnit4ClassRunner.class)
    • JUnit으로 작성한 테스트 케이스를 스프링 프레임워크와 통합하기 위한 것
    • 테스트가 스프링 컨테이너에서 실행되어 제공되는 @Autowired 같은 기능을 사용할 수 있음
  • @ContextConfiguration(locations = "classpath:appConfig.xml")
    • 테스트 케이스 실행 시 사용할 스프링 정보 지정. (여기서는 설정 정보로 appConfig.xml을 설정함)
  • 트랜잭션
    • 테스트에서는 실행 마다 트랜잭션을 시작하고, 테스트 종료 후에는 트랜잭션을 강제로 롤백하기에, 데이터베이스에 저장한 내용이 초기화되어 반복해 테스트를 진행할 수 있음

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:appConfig.xml")
@Transactional
public class MemberServiceTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    public void 회원가입() throws Exception {

        //Given
        Member member = new Member();
        member.setName("kim");

        //When
        Long saveId = memberService.join(member);

        //Then
        assertEquals(member, memberRepository.findOne(saveId));
    }

    @Test(expected = IllegalStateException.class)
    public void 중복_회원_예외() throws Exception {

        //Given
        Member member1 = new Member();
        member1.setName("kim");

        Member member2 = new Member();
        member2.setName("kim");

        //When
        memberService.join(member1);
        memberService.join(member2); //예외가 발생해야 한다.

        //Then
        fail("예외가 발생해야 한다.");
    }

}
profile
이불 밖은 위험해.

0개의 댓글