지난 주 토요일 시험으로 AWS 기초 중의 기초인 'Certified Cloud Practitioner(CLF-002)'를 취득하였다. 합격으로 받은 50% 할인 바우처로 'Certified Solutions Architect - Associate'를 취득할 생각이다. Practitioner의 경우 AWS Builders에서 제공하는 에센셜 위주로 듣고, 샘플 시험이나 기타 문서를 참고하였다.
전체 글
-
AWS Certified Cloud Practitioner 취득
2025.08.06
- JPA 스터디2 - 4 2025.08.03 3
- JPA 스터디2 - 3 2025.07.30 3
- JPA 스터디2 - 2 2025.07.14 2
- JPA 스터디2 - 1 2025.07.13 1
- JPA 스터디1 - 3 2025.07.01
- JPA 스터디1 - 2 2025.06.16 3
- JPA 스터디1 - 1 2025.06.10 3
- Spring 스터디2 - 10 2025.05.31
- [Bash] Apache 설정 파일 일괄 배포 2025.05.30
AWS Certified Cloud Practitioner 취득
JPA 스터디2 - 4
객체와 테이블 매핑
JPA에서는 @Entity
와 @Table
어노테이션을 통해 객체와 테이블을 매핑한다.
@Entity
JPA에서 @Entity
어노테이션은 자바 클래스를 데이터베이스의 테이블과 매핑하기 위해 사용하며, 이 어노테이션이 선언된 클래스는 JPA가 관리하는 영속 객체(Entity)가 된다. 파라미터가 없는 기본 생성자가 필요하며, @Id
로 기본키(Primary Key)를 지정해야한다.
주요 속성
- name:
- 객체의 이름을 지정하며, 지정하지 않으면 클래스명이 이름이 된다.
- JPQL에서 객체를 구분하는 데 사용된다.
@Entity(name = "User")
public class Member {
...
}
@Table
@Table
어노테이션은 객체가 매핑될 실제 데이터베이스 테이블의 이름이나 세부 정보를 지정할 때 사용한다. 보통 @Entity
와 함께 사용되며, 추가적으로 테이블명, 스키마, 인덱스, 고유 제약 조건 등 다양한 정보를 설정할 수 있다.
주요 속성
- name: 매핑할 테이블명 지정, 지정하지 않으면 클래스명이 테이블명이 된다.
- schema: 매핑할 스키마명 지정
- catalog: 매핑할 카탈로그명 지정
- uniqueConstraints: 테이블에 고유 제약 조건(Unique 제약 조건) 설정
- indexes: 테이블에 인덱스 설정
데이터베이스 스키마 자동 생성
JPA는 애플리케이션 실행 시점에 DDL을 자동으로 생성한다. 이때 테이블 중심이 아닌 객체 중심으로 생성하고, persistence.xml
에서 설정한 데이터베이스 방언을 활용하여 데이터베이스에 맞는 적절한 DDL을 생성한다. 하지만 이렇게 생성된 DDL은 개발 환경에서만 사용하고, 운영서버에서는 사용하지 않거나 적절히 다듬은 후 사용해야 한다.
데이터베이스 스키마 자동 생성과 관련해서는 persistence.xml
의 hibernate.hbm2ddl.auto
속성으로 지정할 수 있다. 주의할 점이 있다면 운영 환경에서 create, create-drop, update를 사용하면 절대 안 된다. 'update 정도는 써도 되지 않나?' 싶지만, 주요 테이블에 ALTER 잘못 날리면 서비스 장애가 발생할 가능성이 높다.
주요 속성
- create: 애플리케이션 실행 시 기존 테이블 DROP → 신규 테이블 CREATE
- create-drop: 애플리케이션 실행 시 기존 테이블 DROP → 신규 테이블 CREATE → 종료 시 DROP
- update: 변경된 내용만 ALTER
- validate: 객체와 테이블이 정상 매핑되었는지만 확인
- none: 미사용
필드와 컬럼 매핑
약간의 요구사항이 있었다.
- 회원은 일반회원과 관리자로 구분해야 한다.
- 회원가입일과 수정일이 있어야한다.
- 회원을 설명할 수 있는 필드가 있으며, 이 필드는 길이 제한이 없다.
...
@Entity
public class Member {
@Id
private Long id;
@Column(name = "username")
private String name;
private Integer age;
@Enumerated(EnumType.STRING)
private RoleType roleType;
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
@Lob
private String description;
}
필드와 컬럼 매핑 시 사용하는 어노테이션과 주요 속성이다.
- @Column: 필드와 컬럼 매핑. 세부 속성값을 설정하여 구체적으로 지정가능하고 생략 가능(생략 시 기본값 적용)
- name: 매핑할 컬럼 이름 지정(기본값: 필드명)
- nullable: 컬럼의 NULL 허용 여부 지정(기본값: true)
- unique: 고유 제약 조건 생성(기본값: false)
- length: 문자열 컬럼의 최대 길이 지정(String 타입에만 적용 / 기본값: 255)
- columnDefinition: 데이터베이스 컬럼 정의 직접 입력(SQL 타입 및 디폴트 등)
- insertable/updatable: 해당 필드를 INSERT/UPDATE SQL에 포함시킬지 여부(기본값: true)
- precision/scale: BigDecimal/BigInteger 타입에서 전체 자릿수, 소수점 이하 자릿수 지정
- table: 여러 테이블에 매핑할 때 사용하는 옵션
- @Enumerated: enum 타입 필드와 컬럼 매핑
- value: 기본값 EnumType.ORDINAL이나 ORDINAL은 사용하지 않는 것을 권장
- EnumType.ORDINAL: enum 순서를 데이터베이스에 저장(0, 1, 2 ...)
- EnumType.STRING: enum 이름을 데이터베이스에 저장
- value: 기본값 EnumType.ORDINAL이나 ORDINAL은 사용하지 않는 것을 권장
- @Temporal: 날짜/시간 타입 필드와 컬럼 매핑(java.util.Date, java.util.Calendar)
- @Lob: Large Object 필드와 컬럼 매핑(String→CLOB, byte[]→BLOB)
- @Transient: 필드를 컬럼과 매핑하지 않음(메모리상에서 임시로 사용하는 필드에 사용)
기본키 매핑
기본키 매핑 방법은 @Id
어노테이션만 사용하여 직접 값을 입력하거나, @GeneratedValue
어노테이션을 사용하여 자동으로 생성하여 입력할 수 있다. @GeneratedValue
어노테이션의 주요 전략은 IDENTITY, SEQUENCE, TABLE, AUTO 4가지가 있다.
- IDENTITY: 데이터베이스에 위임
- 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용
- JPA는 트랜잭션 커밋 시점에 INSERT SQL 실행
- IDENTITY 전략 사용 시 em.persist() 시점에 즉시 INSERT SQL 실행하여 DB에서 ID값 조회
- SEQUENCE: 데이터베이스 스퀀스 오브젝트 사용
- 주로 ORACLE, PostgreSQL, DB2, H2에서 사용
- @SequenceGenerator 필요
- name: 식별자 생성기 이름
- sequenceName: 데이터베이스에 등록되어 있는 시퀀스 이름
- initialValue: DDL 생성 시에만 사용되며, 처음 시작하는 수 지정
- allocationSize: 시퀀스 한 번 호출에 증가하는 수(기본값: 50)
- 데이터베이스 스퀀스 설정이 하나씩 증가이면, 반드시 1로 설정
- catalog, schema: 데이터베이스 catalog, schema 이름
- TABLE: 키 생성용 테이블 사용
- 모든 데이터베이스에서 사용 가능하나, 성능이 좋지 못함
- @TableGenerator 필요
- name: 식별자 생성기 이름
- table: 키 생성 테이블 이름(기본값: hibernate_sequences)
- pkColumnName: 시퀀스 컬럼 이름(기본값: sequence_name)
- valueColumnName: 시퀀스 값 컬럼 이름(기본값: next_val)
- pkColumnValue: 키로 사용할 값 이름(기본값: 객체명)
- initialValue: 초기값, 마지막으로 생성된 값 기준(기본값: 0)
- allocationSize: 시퀀스 한 번 호출에 증가하는 수(기본값: 50)
- catalog, schema: 데이터베이스 catalog, schema 이름
- uniqueConstraints: 고유 제약 조건 지정
- AUTO: 데이터베이스 방언에 따라 자동 지정(기본값)
실전 예제
요구사항 분석과 기본 매핑인데, 강의에서 신규 프로젝트 생성하였으나 테스트 하던 프로젝트에 추가했다.
https://github.com/hongbre/jpa-basic
GitHub - hongbre/jpa-basic
Contribute to hongbre/jpa-basic development by creating an account on GitHub.
github.com
'스터디 > JPA' 카테고리의 다른 글
JPA 스터디2 - 3 (3) | 2025.07.30 |
---|---|
JPA 스터디2 - 2 (2) | 2025.07.14 |
JPA 스터디2 - 1 (1) | 2025.07.13 |
JPA 스터디1 - 3 (0) | 2025.07.01 |
JPA 스터디1 - 2 (3) | 2025.06.16 |
JPA 스터디2 - 3
영속성 컨텍스트
이전에 얘기했듯 웹 애플리케이션에서는 하나의 EntityManagerFactory를 생성하고, 사용자의 요청에 따라 EntityManager를 생성하여 트랜잭션을 처리한다. 영속성 컨텍스트(Persistence Context)는 애플리케이션 내에서 객체 인스턴스를 일시적으로 보관하고 관리하는 논리적 환경이다. JPA를 이해하는데 가장 중요한 단어이다.
영속성 컨텍스트는 쉽게 말해 가상의 데이터베이스 역할을 하는 논리적인 개념으로, 눈에 보이지 않는다. EntityManager가 생성될 때 같이 생성되며, 보통 하나의 트랜잭션 범위 동안 유지된다. 객체의 생명주기는 비영속(New/Transient), 영속(Managed), 준영속(Detached), 삭제(Removed)로 나뉜다.
- 비영속(New/Transient): 객체가 새로 생성만 되어 영속성 컨텍스트와 연관이 없는 상태
- 영속(Managed): persist, find 등으로 영속성 컨텍스트에서 관리되는 상태
- 준영속(Detached): 영속성 컨텍스트에서 분리된 상태
- 영속성 컨텍스트가 제공하는 기능을 사용 못하게 된다.
em.detach(객체)
,em.clear()
,em.close()
로 준영속 상태로 만들 수 있다.- 각각 특정 객체만 전환, 전체 초기화, 종료의 동작을 한다.
- 삭제(Removed): 영속성 컨텍스트에서 삭제된 상태
영속성 컨텍스트의 장점(또는 주요 특징)으로는 1차 캐시, 동일성(Identity) 보장, 쓰기 지연(Write-behind), 변경 감지(Dirty Checking), 지연 로딩(Lazy Loading)이 있다.
- 1차 캐시: 컨텍스트 내부에 객체들이 캐시되어 있어, 동일한 객체를 여러번 조회해도 최초 한 번만 DB에서 읽고, 이후에는 영속성 컨텍스트에서 조회한다. 영속성 컨텍스트에 없는 객체를 find하면 DB에서 조회 후 1차 캐시에 저장한다.
- 동일성 보장: 영속성 컨텍스트 내에서 같은 PK를 가지는 객체는 항상 같인 객체로 식별된다.
- 쓰기 지연: persist() 나 객체 추가 작업들이 먼저 영속성 컨텍스트에 저장되고, 실제 DB에는 트랜잭션 커밋 시 일괄로 반영된다. 이때 쓰기 지연 SQL 저장소에 쿼리를 저장하고 있다가 한 번에 실행한다.
- 변경 감지: 트랜잭션 내에서 객체의 값이 변경되면, 커밋 시점에 자동으로 변경 내용을 탐지해 DB에 반영한다.
쓰기 지연에서 '트랜잭션 커밋 시 일괄로 반영된다'고 했는데, 이때 flush가 실제로 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다. 커밋 할 때 flush가 자동(jpql 쿼리 실행 시에도)으로 호출되며, em.flush()
로 직접 호출할 수도 있다. flush는 이름과 다르게 영속성 컨텍스트를 비우는 것이 아니며, 객체는 계속 영속 상태를 유지한다. flush가 발샐하면 다음 단계로 진행된다.
- 변경 감지로 변경된 객체를 찾는다.
- 쓰기 지연 SQL 저장소에 변경 SQL(INSERT, UPDATE, DELETE)를 등록한다.
- 등록된 쿼리를 실제 DB에 전송하여 반영한다.
'스터디 > JPA' 카테고리의 다른 글
JPA 스터디2 - 4 (3) | 2025.08.03 |
---|---|
JPA 스터디2 - 2 (2) | 2025.07.14 |
JPA 스터디2 - 1 (1) | 2025.07.13 |
JPA 스터디1 - 3 (0) | 2025.07.01 |
JPA 스터디1 - 2 (3) | 2025.06.16 |
JPA 스터디2 - 2
프로젝트 생성
H2 데이터베이스는 기존에 사용하고 있었기에 신규 DB 파일만 생성하고 Java 프로젝트를 생성한다. 강의에서는 Java 8에 빌드도 Maven으로 설정하고 있으나, 깔린대로 해도 문제가 없을거 같아서 Java 21에 빌드는 Gradle로 설정했다.
plugins {
id("java")
}
group = "jpa-basic"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
//spring-boot-starter-jpa 를 보통 많이 쓴다고 하지만,
//강의에서는 maven 에 hibernate 와 h2 database 를 따로 추가했다.
implementation("org.hibernate.orm:hibernate-core:7.0.6.Final") //hibernate
implementation("com.h2database:h2:2.3.232") //h2 database
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
}
tasks.test {
useJUnitPlatform()
}
JPA 구현체인 Hibernate와 사용 할 데이터베이스인 H2 데이터베이스의 의존성을 추가해준다. 스프링 부트였다면 spring-boot-starter-jpa
만 추가해도 두 개 다 포함하고 있으나, 이번 강의는 스프링을 사용하지 않기 때문에 제외한다.
JPA 설정 파일
JPA를 사용하기 위해서는 persistence.xml
이라는 설정 파일이 필요하다. 기본적으로 META-INF
디렉토리 하위에 위치하고, persistence-unit
의 name
으로 이름을 지정하여 사용한다. 속성 중에 javax.persistence
로 시작하는 속성은 JPA 표준 속성으로 구현체의 영향을 받지 않고, hibernate
로 시작하는 속성은 Hibernate에서만 사용할 수 있는 속성이다.
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="hello">
<!-- 순수한 JPA 프로젝트에서는 Entity 클래스를 명시적으로 등록 -->
<class>hellojpa.Member</class>
<properties>
<!-- 필수 속성 -->
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="javax.persistence.jdbc.user" value="sa"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/Downloads/spring/jpa-basic/jpa"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<!-- 옵션 -->
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
<!--<property name="hibernate.hbm2ddl.auto" value="create" />-->
</properties>
</persistence-unit>
</persistence>
JPA Dialect(방언)
JPA의 Dialect(방언)은 JPA 구현체가 어떤 데이터베이스에 맞는 SQL을 생성해야 하는지 알려주는 설정이다. JPA는 특정 데이터베이스에 종속되어있지 않고, SQL은 표준이 있지만 DB마다 문법·함수·데이터타입 등이 다르기 때문에 필요하다. 위의 JPA 설정 파일에서도 hibernate.dialect
속성을 H2 데이터베이스에 맞추었다. 만약 MySQL이나 Oracle을 사용한다면 사용하는 데이터베이스에 맞추면 된다.
기본 코드 생성
package hellojpa;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;
import java.util.List;
public class JpaMain {
public static void main(String[] args) {
//애플리케이션에 하나만 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
//트랜잭션 필요 시 생성 및 close, 쓰레드 간 공유 X
EntityManager em = emf.createEntityManager();
//JPA 의 모든 데이터 변경은 트랜잭션 안에서 실행
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
/* Create
Member member = new Member();
member.setId(2L);
member.setName("2Name");
em.persist(member);
*/
/* Update
//JPA 를 통해서 객체를 가져오면 persist 할 필요가 없다.
Member findMember = em.find(Member.class, 1L);
findMember.setName("Name1");
*/
List<Member> result = em.createQuery("select m from Member as m", Member.class)
.getResultList();
for(Member member : result) {
System.out.println("member.name = " + member.getName());
}
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
JpaMain.java
를 생성하고 EntityManagerFactory
와 EntityManager(em)
를 생성하고, 생성한 em
에서 트랜잭션을 얻어와 그 안에서 데이터 변경을 실행한다. 트랜잭션이 종료되면 결과에 따라 commit 또는 rollback하게 되고, em
을 반환하게 된다. 주석으로 작성하였 듯이 EntityManagerFactory
는 애플리케이션에 하나만 생성하고, em
은 트랜잭션 필요 시 생성하고 반환하면 된다.
=> EntityManagerFactory
는 데이터베이스 연결 설정, JPA 메타데이터 파싱, EntityManager 생성 등 무거운 리소스를 관리하는 역할이다. 생성 비용이 매우 크고, 내부적으로 커넥션 풀, 캐시, 설정 정보 등을 보유한다. 또한 여러 스레드에서 동시에 접근해도 안전하게 설계(Thread-safe)되어 있지만, 여러 개를 만들면 시스템 자원을 낭비하고, 성능 저하 및 예기치 않은 동시성 문제가 발생할 수 있어 애플리케이션에 하나만 생성하고 공유하는 것이 가장 효율적이다.
=> EntityManager
는 실제로 Entity를 관리(CRUD)하며, 영속성 컨텍스트※를 관리한다. 생성 비용이 크지 않고, 가볍다. 하지만 여러 스레드에서 동시에 접근하면 데이터 불일치, 예기치 않은 버그가 발생할 수 있다(Non-thread-safe). EntityManager
는 트랜잭션 범위 내에서 Entity의 상태를 관리하는데, 트랜잭션이 끝나면 EntityManager
와 함께 영속성 컨텍스트도 종료되어야 일관성이 보장된다. 다음과 같은 이유로 필요 시 생성하고, 공유하지 않는다.
- 동시성 문제: 여러 트랜잭션이 하나의 `EntityManager`를 공유하면, 서로 다른 작업이 섞여 데이터 정합성이 깨진다.
- 영속성 컨텍스트 오염: 이전 트랜잭션의 데이터가 남아있어 잘못된 결과를 반환할 수 있다.
- JPA 명세: `EntityManager`는 트랜잭션 단위로 생성/사용/종료하는 것이 명확하게 권장된다.
※ 영속성 컨텍스트(Persistence Context)는 JPA에서 Entity 객체를 영구적으로 저장하고 관리하는 메모리 공간을 의미하는 핵심 개념 중 하나이다. Entity 생명주기 관리, 1차 캐시, 동일성 보장, 변경 감지(Dirty Checking), 쓰기 지연(Write-behind), 지연 로딩(Lazy Loading) 등의 주요 특징이 있다.
package hellojpa;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity
public class Member {
@Id
private Long id;
private String name;
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;
}
}
@Entity
어노테이션이 있어야 JPA가 관리하는 객체가 된다. 순수한 Java 프로젝트를 생성했기 때문에 persistence.xml
에 <class>hellojpa.Member</class>
와 같이 Entity 클래스를 등록해줘야 인식한다.
JPQL
JPQL(Java Persistence Query Language)는 JPA에서 사용하는 객체지향 쿼리 언어다. 가장 큰 특징은 데이터베이스의 테이블이 아니라 Entity 객체를 대상으로 쿼리를 작성한다는 점인데, 검색도 테이블이 아니라 Entity 객체를 대상으로 검색한다. SQL을 추상화하기 때문에 특정 데이터베이스에 종속되지 않으며, JPA가 JPQL을 실제 DB에 맞는 SQL로 변환해서 실행한다.
...
List<Member> result = em.createQuery("select m from Member as m", Member.class)
.getResultList();
...
위에서 작성된 모든 Member Entity를 검색하는 JPQL이다.
https://github.com/hongbre/jpa-basic
GitHub - hongbre/jpa-basic
Contribute to hongbre/jpa-basic development by creating an account on GitHub.
github.com
'스터디 > JPA' 카테고리의 다른 글
JPA 스터디2 - 4 (3) | 2025.08.03 |
---|---|
JPA 스터디2 - 3 (3) | 2025.07.30 |
JPA 스터디2 - 1 (1) | 2025.07.13 |
JPA 스터디1 - 3 (0) | 2025.07.01 |
JPA 스터디1 - 2 (3) | 2025.06.16 |
JPA 스터디2 - 1
JPA란 무엇인가?
JPA는 Java Persistence API의 약자로 자바 객체를 관계형 데이터베이스에 저장하는 방식을 표준화한 자바 명세이다. ORM을 위한 인터페이스와 어노테이션을 정의하며, 자바 클래스를 데이터베이스 테이블에 매핑하거나 SQL 대신 객체지향 코드로 데이터베이스 CRUD 작업을 수행한다. JPA 자체는 '명세'이고, 실제로는 Hibernate, EclipseLink 등과 같은 구현체를 통해 동작한다. 이런 JPA는 데이터베이스 접근을 단순화하고, 생산성을 높이며, 객체지향 자바 코드와 관계형 데이터베이스 간의 간극을 줄여준다.
public class Member {
private String memberId;
private String name;
private String tel; // 기존에 없던 필드가 추가되었을 때
...
}
JPA를 사용하지 않을 때는 tel
이라는 필드가 추가되면 해당 테이블을 조회하는 모든 SQL을 찾아서 수정해야하지만, JPA를 사용하면 필드 추가 이후 SQL은 JPA가 알아서 생성해준다.
지연 로딩과 즉시 로딩
지연 로딩(Lazy Loading)은 연관된 엔티티를 실제로 사용하는 시점까지 데이터베이스에서 조회를 미루는 방식으로, Member
엔티티에서 Team
엔티티를 지연 로딩으로 설정하면, Member
를 조회할 때는 Team
데이터가 실제로 필요할 때까지 쿼리가 실행되지 않는다. member.getTeam()
호출 등 실제로 Team
데이터가 필요해질 때 추가 쿼리가 실행된다.
=> 불필요한 데이터 조회를 방지해 성능을 향상시킬 수 있고, 메모리 사용량을 줄일 수 있다는 장점이 있다. 단점으로는 연관 객체를 사용하는 시점에 추가 쿼리가 발생하므로, 트랜잭션이 종료된 후 접근하면 LazyInitializationException
이 발생할 수 있다.
즉시 로딩(Eager Loading)은 연관된 모든 엔티티도 함께 즉시 조회하는 방식으로, Member
엔티티에서 Team
엔티티를 즉시 로딩으로 설정하면, Member
를 조회할 때 Team
도 함께 조인 쿼리로 조회된다.
=> 연관 객체를 바로 사용할 수 있고, 트랜잭션 종료 후에도 이미 데이터가 로딩되어 있어 예외가 발생하지 않는다는 장점이 있다. 단점으로는 연관된 데이터가 많을 경우 불필요하게 많은 데이터를 한 번에 가져와 성능 저하 및 메모리 낭비가 발생할 수 있고, 복잡한 연관관계에서는 N+1 문제 등 예기치 않은 쿼리 폭증이 발생할 수 있다.
대부분에 실무에서는 지연 로딩 사용이 권장된다.
패러다임 불일치 해결
객체지향 프로그래밍과 관계형 데이터베이스 사이에는 패러다임의 불일치라는 근본적인 차이가 존재한다. 객체지향은 데이터를 객체와 메소드로 구성하며, 상속·다형성 등 계층적 구조를 지원한다. 관계형 데이터베이스는 데이터를 행과 열로 구조화하며, 상속이나 객체 참조 개념이 없다. 이러한 차이로 인해 다음과 같은 문제가 발생한다.
- 상속 미지원: 객체지향의 상속 구조를 테이블로 직접 표현할 수 없다
- 참조와 식별의 차이: 객체는 참조로 연결되지만, RDB는 외래키로 연결된다
- 데이터 구조의 불일치: 객체는 중첩, 컬렉션 등 다양한 구조를 가지지만, RDB는 단순한 테이블 구조이다
- 데이터 접근 방식의 차이: 객체는 관계를 따라 탐색하지만, RDB는 조인으로 데이터를 결합한다
JPA는 패러다임 불일치 문제를 다음과 같이 해결한다.
- 매핑 전략 제공
- 상속 매핑: 객체의 상속 구조를 테이블에 저장할 수 있도록 여러 매핑 전략을 제공한다
- 연관관계 매핑: 객체 간의 참조를 외래키로 변환하여 테이블 간 관계를 자동으로 관리한다
- 객체 중심 개발 지원
- 개발자는 자바 컬렉션에 객체를 저장하듯이 엔티티를 다루면, JPA가 내부적으로 SQL을 생성해 DB에 반영한다
- 복잡한 SQL 작성과 매핑 코드를 줄여 생산성을 높이고, 객체 모델링에 집중할 수 있다
- 데이터 변환 자동화
- 객체와 ㅌ테이블 간의 데이터 변환을 자동으로 처리하여, 개발자는 객체만 신경쓰면 된다
- JPQL 등 객체지향 쿼리 제공
- SQL 대신 객체를 대상으로 하는 JPQL 등 객체지향 쿼리를 지원해 객체 모델에 맞는 데이터 접근이 가능하다
따라서 상속, 다형성, 컬렉션 등 객체지향 개념을 데이터베이스에 자연스럽게 반영하므로 반복적인 SQL 및 매핑 코드가 감소되어 개발 생산성이 향상되고, 객체지향적 설계와 데이터베이스 설계의 간극이 해소된다. 또한 데이터베이스 벤더에 종속되지 않는 이식성을 확보할 수 있는 효과가 있다.
'스터디 > JPA' 카테고리의 다른 글
JPA 스터디2 - 3 (3) | 2025.07.30 |
---|---|
JPA 스터디2 - 2 (2) | 2025.07.14 |
JPA 스터디1 - 3 (0) | 2025.07.01 |
JPA 스터디1 - 2 (3) | 2025.06.16 |
JPA 스터디1 - 1 (3) | 2025.06.10 |
JPA 스터디1 - 3
https://github.com/hongbre/spring-jpashop
GitHub - hongbre/spring-jpashop
Contribute to hongbre/spring-jpashop development by creating an account on GitHub.
github.com
'실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발' 강의를 쭉 들었는데, Spring 기본만 하고 JPA 기본은 안 하니 이해가 안 된다. JPA 기본 먼저 듣고 강의 다시 들으면서 정리하기로 했다.
'스터디 > JPA' 카테고리의 다른 글
JPA 스터디2 - 3 (3) | 2025.07.30 |
---|---|
JPA 스터디2 - 2 (2) | 2025.07.14 |
JPA 스터디2 - 1 (1) | 2025.07.13 |
JPA 스터디1 - 2 (3) | 2025.06.16 |
JPA 스터디1 - 1 (3) | 2025.06.10 |
JPA 스터디1 - 2
요구사항 분석
기능목록
- 회원 기능
- 회원 등록
- 회원 조회
- 상품 기능
- 상품 등록
- 상품 수정
- 상품 조회
- 주문 기능
- 상품 주문
- 주문 내역 조회
- 주문 취소
- 기타 요구사항
- 상품은 재고 관리가 필요하다.
- 상품은 카테고리로 구분할 수 있고 도서, 음반, 영화가 있다.
- 상품 주문 시 배송 정보를 입력할 수 있다.
도메인 모델과 테이블 설계
도메인
엔티티
테이블
엔티티 클래스 개발
https://github.com/hongbre/spring-jpashop
GitHub - hongbre/spring-jpashop
Contribute to hongbre/spring-jpashop development by creating an account on GitHub.
github.com
- 다중성(관계의 수) 기준
- OneToOne(1:1)
- 하나의 엔티티가 다른 엔티티와 오직 하나의 관계만 가질 때 사용
- ORDERS와 DELIVERY
- ManyToOne(N:1)
- 여러 엔티티가 하나의 엔티티와 관계를 맺을 때 사용
- ORDER_ITEM과 ITEM
- OneToMany(1:N)
- 하나의 엔티티가 여러 엔티티와 관계를 맺을 때 사용
- ORDERS와 ORDER_ITEM
- ManyToMany(N:N)
- 여러 엔티티가 서로 다수와 관계를 맺을 때 사용
- 실무에서는 사용을 지양하고, 중간 엔티티를 만들기를 권장
- ITEM과 CATEGORY
- OneToOne(1:1)
- 방향 및 연관관계의 주인
- 단방향
- 한쪽 엔티티만 다른 엔티티를 참조
- ORDER_ITEM과 ITEM의 관계만 단방향
- 양방향
- 양쪽 엔티티가 서로를 참조
- 연관관계의 주인(Owner)
- 외래키(FK)를 가진 엔티티가 연관관계의 주인이 된다
- 주인 엔티티에서만 연관관계의 변경(CUD)이 가능
- 단방향
'스터디 > JPA' 카테고리의 다른 글
JPA 스터디2 - 3 (3) | 2025.07.30 |
---|---|
JPA 스터디2 - 2 (2) | 2025.07.14 |
JPA 스터디2 - 1 (1) | 2025.07.13 |
JPA 스터디1 - 3 (0) | 2025.07.01 |
JPA 스터디1 - 1 (3) | 2025.06.10 |
JPA 스터디1 - 1
Spring initializr로 프로젝트 생성
- Porject: Gradle-Groovy
- Language: Java
- Spring Boot: 3.5.0
- Project Metadata:
- Group: jpabook
- Artifact: jpashop
- Name: jpashop (기본적으로 Artifact 동일)
- Description: Demo Project for Spring Boot (기본값)
- Package Name: jpabook.jpashop (Group.Artifact)
- Packaging: Jar
- Java: 21
- Dependencies
- Spring Web (웹 애플리케이션 개발 필수)
- Lombok (getter/setter 등 어노테이션으로 생성)
- Thymeleaf (템플릿 엔진)
- Spring Data JPA (JPA 사용을 위함)
- H2 Database (개발용으로 많이 쓰는 데이터베이스)
- Validation (자바 빈 객체의 값 검증)
Lombok 적용 테스트
package jpabook.jpashop;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class LombokTest {
private String data;
}
@Getter
와 @Setter
를 통해 data 값을 넣고 조회하는 LombokTest 클래스 생성
package jpabook.jpashop;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class JpashopApplication {
public static void main(String[] args) {
// Lombok 적용 테스트
LombokTest lombokTest = new LombokTest();
lombokTest.setData("Test");
System.out.println(lombokTest.getData());
SpringApplication.run(JpashopApplication.class, args);
}
}
콘솔에 "Test"
가 출력되며 확인 완료
JPA와 DB 설정
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/Downloads\spring\springjpa1-v20250325/jpashop
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
# show_sql: true
format_sql: true
logging:
level:
org.hibernate.SQL: debug
org.hibernate.orm.jdbc.bind: trace
H2 데이터베이스와 연결하고, JPA를 설정한다. spring.jpa.hibernate.ddl-auto의 값은 다음과 같다.
- none: 스키마를 자동으로 생성하거나 변경하지 않음
- validate: 엔티티와 스키마가 일치하는지 검증만 하며 변경하지 않음
- update: 엔티티 변경사항을 반영해 스키마를 자동 갱신함(기존 데이터 유지, 컬럼 삭제 등은 미반영)
- create: 기존 스키마를 삭제하고 엔티티 기준으로 새로 생성(기존 데이터 모두 삭제)
- create-drop: create와 동일하게 시작 시 새로 생성하고 종료 시 스키마 삭제
'스터디 > JPA' 카테고리의 다른 글
JPA 스터디2 - 3 (3) | 2025.07.30 |
---|---|
JPA 스터디2 - 2 (2) | 2025.07.14 |
JPA 스터디2 - 1 (1) | 2025.07.13 |
JPA 스터디1 - 3 (0) | 2025.07.01 |
JPA 스터디1 - 2 (3) | 2025.06.16 |
Spring 스터디2 - 10
스프링 빈 스코프
스프링 빈 스코프(Bean Scope)는 스프링 컨테이너가 관리하는 객체인 빈이 생성되고 소멸될 때까지 존재할 수 있는 범위나 생명주기이다. 주요 스코프 종류는 다음과 같다.
- 싱글톤(Singleton) : 기본값으로 스프링 컨테이너 시작부터 종료까지 단 하나의 인스턴스만 생성, 모든 요청에 동일 객체를 반환
- 프로토타입(Prototype) : 빈을 요청할 때마다 새로운 인스턴스 생성. 생성과 의존성 주입까지만 컨테이너가 관리하며 이후는 클라이언트가 직접 관리 필요
- request : HTTP 요청마다 새로운 빈 생성, 요청이 끝나면 소멸한다. 주로 웹 애플리케이션에서 사용
- session : HTTP 세션마다 새로운 빈 생성, 세션 종료 시 소멸한다. 주로 사용자별 데이터 관리에 사용
- application : 서블릿 컨텍스트와 동일한 생명주기를 가지며, 웹 애플리케이션 전체에서 공유
- websocket : 웹소켓 연결마다 빈 생성, 연결 종료 시 소멸한다.
프로토타입 스코프
- 싱글톤 스코프의 빈을 스프링 컨테이너에 요청한다.
- 스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환한다. (3개의 클라이언트 동일)
- 프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.
- 스프링 컨테이너는 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.
- 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환한다.
- 이후에 빈을 관리하지 않으며, 3개의 클라이언트는 다른 빈을 받는다.
싱글톤 스코프 빈 테스트
package spring.basic.scope;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import static org.assertj.core.api.Assertions.*;
public class SingletonTest {
@Test
void singletonBeanFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
SingletonBean singletonBean1 = ac.getBean(SingletonBean.class); //최초 호출 시 init 실행
SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
System.out.println("singletonBean1 = " + singletonBean1);
System.out.println("singletonBean2 = " + singletonBean2);
assertThat(singletonBean1).isSameAs(singletonBean2); //둘은 같은 객체이다
ac.close(); //종료 시 destroy 실행
}
@Scope("singleton") //기본 Scope 는 singleton 이다
static class SingletonBean {
@PostConstruct
public void init() {
System.out.println("SingletonBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("SingletonBean.destroy");
}
}
}
프로토타입 스코프 빈 테스트
package spring.basic.scope;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import static org.assertj.core.api.Assertions.assertThat;
public class PrototypeTest {
@Test
void prototypeBeanFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
System.out.println("find prototypeBean1");
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class); //호출 시 init 실행
System.out.println("find prototypeBean2");
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class); //호출 시 init 실행
System.out.println("prototypeBean1 = " + prototypeBean1);
System.out.println("prototypeBean2 = " + prototypeBean2);
assertThat(prototypeBean1).isNotSameAs(prototypeBean2); //둘은 다른 객체이다
ac.close(); //종료 시 destroy 를 실행하지 않는다
}
@Scope("prototype") //Scope 를 prototype 으로 변경
static class PrototypeBean {
@PostConstruct
public void init() {
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}
프로토타입과 싱글톤을 함께 사용할 때 문제점
일반적으로 프로토타입 스코프를 사용하는 이유는 클라이언트가 요청할 때마다 인스턴스를 생성하기 위해서일 것이다. 하지만 스프링은 싱글톤 빈의 의존성 주입을 생성할 때 한 번만 주입한다. 따라서 싱글톤 빈에 프로토타입 스코프를 가진 빈을 주입 받아서 사용할 경우 주입 시점에 프로토타입 빈이 생성되어 주입되며 이후에 같은 인스턴스를 사용하게 된다(프로토타입 빈의 의미가 사라진다). 이 문제점을 해결하기 위해서는 ObjectFactory 또는 Provider를 사용할 수 있다.
package spring.basic.scope;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Provider;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
public class SingletonWithPrototypeTest1 {
@Test
void prototypeFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
prototypeBean1.addCount();
assertThat(prototypeBean1.getCount()).isEqualTo(1);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
prototypeBean2.addCount();
assertThat(prototypeBean2.getCount()).isEqualTo(1);
}
@Test
void singletonClientUsePrototype() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int count1 = clientBean1.logic();
assertThat(count1).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean2.logic();
assertThat(count2).isEqualTo(1);
}
@Scope("singleton")
static class ClientBean {
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
@Scope("prototype")
static class PrototypeBean {
private int count = 0;
public void addCount() {
count++;
}
public int getCount() {
return count;
}
@PostConstruct
public void init() {
System.out.println("PrototypeBean.init " + this);
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}
웹 스코프
웹 스코프는 웹 환경에만 동작하며, 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 종류는 위에서 말했던 것 중 request, session, application, websocket이 있다. 이번에는 request 스코프를 통해 로그를 남기는 예제를 작성했다.
MyLogger
package spring.basic.common;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
private String uuid;
private String requestURL;
public void setRequestURL(String requestURL) {
this.requestURL = requestURL;
}
public void log(String message) {
System.out.println("[" + uuid + "] [" + requestURL + "] " + message);
}
@PostConstruct
public void init() {
uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] request scope bean create:" + this);
}
@PreDestroy
public void close() {
System.out.println("[" + uuid + "] request scope bean close:" + this);
}
}
@Scope(value = "request")
로 request 스코프로 지정하였기 때문에 이 빈은 HTTP 요청 당 하나의 인스턴스가 생성되고, HTTP 요청 종료 시 소멸된다. 빈 생성 시 @PostConstructor
를 통해 UUID를 만들어서 저장하고, 빈 소멸 시 @PreDestroy
를 통해 소멸 메시지를 남긴다. requestURL
은 빈 생성 당시에는 알 수 없기 때문에 setter로 입력받는다.
LogDemoController
package spring.basic.web;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import spring.basic.common.MyLogger;
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
//MyLogger myLogger = myLoggerProvider.getObject();
System.out.println("myLogger = " + myLogger.getClass());
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
MyLogger가 제대로 동작하는지 확인하기 위한 컨트롤러로, log-demo
로 요청한다.
LogDemoService
package spring.basic.web;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import spring.basic.common.MyLogger;
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
// MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + id);
}
}
요청에 따른 로그가 정상적으로 출력된다.
MyLogger에서 @Scope
의 proxyMode를 ScopedProxyMode.TARGET_CLASS를 설정해놨는데, 이는 MyLogger를 프록시 객체로 생성하고, 내부에서는 진짜 myLogger를 호출하게 된다. 이전에 나왔던 프록시 처럼 CGLIB을 통해 처리된다. Provider나 프록시나 진짜 객체 조회를 필요한 시점까지 지연처리 한다는 점이 중요하다.
'스터디 > Spring' 카테고리의 다른 글
Spring 스터디2 - 9 (0) | 2025.05.13 |
---|---|
Spring 스터디2 - 8 (0) | 2025.05.11 |
Spring 스터디2 - 7 (0) | 2025.04.29 |
Spring 스터디2 - 6 (0) | 2025.04.27 |
Spring 스터디2 - 5 (1) | 2025.04.19 |
[Bash] Apache 설정 파일 일괄 배포
아파치를 Docker로 돌리고 싶지만 사정상 서버에 직접 설치해서 운영 중이다. 이번에 다른 목적으로 WEB 서버 6대가 추가되었는데, 6개의 콘솔을 열어서 설정파일을 수정하고 재기동하는건 말도 안 되게 귀찮기 때문에 Bash 스크립트를 작성했다.
ssh 키 쌍 생성하여 패스워드 입력 없이 서버 연결 설정
# 1.키 쌍 생성
$ ssh-keygen -t rsa
# Enter file in which to save the key : 엔터 (기본 경로 ~/.ssh 사용)
# Enter passphrase : 엔터 (패스워드 없이 사용)
# Enter same passphrase again : 엔터
# 2. 생성된 공개키를 대상 서버에 복사
$ ssh-copy -id -p 22 username@remote.server.ip
# 3. 연결 확인
$ ssh -p 22 username@remote.server.ip
ssh 연결하여 명령 실행
# 한 개의 명령 실행
$ ssh -p 22 username@remote.server.ip "touch test"
# 여러 개의 명령 실행(세미콜론으로 구분)
$ ssh -p 22 username@remote.server.ip "touch test; mv test tset"
스크립트 작성
- 설정 파일의 수정이 있을 때
파일명.YYYYMMDD
형태로 파일을 백업한다. - conf.d의 ssl.conf, vhost.conf, workers.properties 위주로 수정한다.
스크립트 작성의 전제 조건이다.
#!/bin/bash
APACHE_CONFD_PATH=/etc/httpd/conf.d
DATE=`date +%Y%m%d`
# 오늘 날짜로 수정된 파일명
MODIFIED_CONF_LIST=`ls -1 ${APACHE_CONFD_PATH} | grep ${DATE} | sed 's/\.[^.]*$//`
# 대상 서버 목록
SERVERS="192.168.0.2 192.168.0.3 192.168.0.4 192.168.0.5 192.168.0.6"
# 수정된 파일별로 반복
for CONF in ${MODIEFIED_CONF_LIST}
do
# 대상 서버별로 파일 전송
for SERVER in ${SERVERS}
do
scp -P 22 ${APACHE_CONFD_PATH}/${CONF} apache@${SERVER}:${APACHE_CONFD_PATH}/${CONF}
scp -P 22 ${APACHE_CONFD_PATH}/${CONF}.${DATE} apache@${SERVER}:${APACHE_CONFD_PATH}/${CONF}.${DATE}
done
done
# 아파치 재기동
for SERVER in ${SERVERS}
do
ssh -n -p 22 apache@${SERVER} "sudo /home/apache/apache_restart.sh"
sleep 5
done
sudo /home/apache/apache_restart.sh
기존에는 apache_stop.sh
과 apache_start.sh
를 따로 실행해서 재기동을 했으나, 한 번의 명령으로 재기동 하기 위해 각 서버에 apache_restart.sh
를 추가 작성했다.
#!/bin/bash
# 아파치 종료
sh /home/apache/apache_stop.sh
while true
do
# 프로세스에 httpd가 없으면(내가 실행한 grep 하나만 나오면) 탈출
if [ `ps -ef | grep -c httpd` -eq 1 ]
then
break
fi
sleep 1
done
# 아파치 시작
sh /home/apache/apache_start.sh