Java Persistance Application

여름빛새·2023년 7월 11일

dev-diary

목록 보기
4/8

1. 들어가기에 앞서

1.1. JDBC(Java DataBase Connectivity)

JDBC는 JAVA를 통해 데이터베이스 프로그래밍을 하기 위한 라이브러리입니다. application과 Database를 연결하는 하는 역할을 하지요.


출처 : https://techvidvan.com/tutorials/jdbc-tutorial/

JDK에서는 특정 DBMS에 종속되지 않는 API를 제공 합니다. 다시 말해, 특정 DBMS를 사용 할 때는, 해당 제품의 JDBC Driver만 교체하면 되는 것이죠.

JDBC는 DriverManager, Driver ,Connection ,Statement ,PreparedStatement, CallableStatement ,ResultSet, SQL data 과 같은 클래스와 인터페이스를 지원 합니다.

1.2. ORM(Object-Relational Mapping)

ORM은 '객체-관계 매핑'의 준말입니다.
이는 객체지향그래밍에서의 "객체"와 관계형 데이터베이스에서의 "관계"를 "매핑(연결)"한다는 의미로 보면 되겠습니다.

사실 JAVA와 같은 OOP에서 클래스와 테이블은 서로 간의 호환 가능성을 두고 만들어진 것이 아니므로 불일치가 발생하는데, ORM을 통해서는 객체 간 관계를 바탕으로 SQL 쿼리문을 자동 생성하여 불일치를 해결합니다. 다시 말해 별도의 SQL 쿼리문이 필요 없이 객체를 통해 간접적으로 데이터베이스를 조작할 수 있게 됩니다.

그렇다면, 왜 ORM이란 개념이 생기게 된 것일까요?

개념

애초에 OOP에서의 객체 개념과 RDBMS의 테이블은 서로의 호환 가능성을 두고 만들어진 것이 아닙니다.

상속 관계

객체 간에는 상속 관계가 존재 하지만, 테이블 간에는 상속 관계가 존재하지 않습니다.
객체는 참조를 통해 관계를 성립합니다.
테이블 간에는 Foreign Key를 이용한 join, union 등의 관계가 성립하지만, 동등한 테이블 간의 관계일 뿐입니다.

지원하는 데이터 타입이 서로 다릅니다.

또한 개발에 있어 SQL에 종속되는 등의 불편함이 따릅니다. 트랜잭션 로직을 만들 때 마다 쿼리문을 만들어야 하니까요.

2. JPA

JPA는 Java Persistance API의 준말 입니다. 다시 말해, 자바 영속성 API라는 것이죠.
Java 진영에서는 JPA를 ORM의 기술 표준으로 삼고 있으며,
SpringData JPA는 ORM을 통해 JPA를 쉽게 구현하는 인터페이스 모듈을 지원하고 있습니다.

2.1. 영속성의 정의

persistance ; 영원히 계속되는 성질이나 능력.
이 개념 자체에 대한 자세한 이해는 Computer Sciences보단 Cultural Sciences이 더 어울리겠죠.

객체지향 프로그래밍에서 영속성은 데이터를 생성한 프로그램이 종료되어도 사라지지 않는 데이터의 특성을 말합니다.
객체에게 영속성이 주어지지 않으면, 데이터는 메모리에서만 존재하게 되고 프로그램이 종료되면 해당 데이터는 모두 사라지게 됩니다(전력이 끊어지게 되니까요). 이를 방지하기 위해서는 데이터를 파일이나 DB에 영구 저장하게 됩니다. 이를 데이터에 영속성을 부여한다고 표현합니다.

뭔가 어렵게 느껴지는 것 같은데, 쉽게 말해 그냥 DB에 데이터를 저장하는 과정입니다. JPA는 말 그대로 Java에서의 데이터 저장을 구현하는 기술 표준인 것이죠.

2.2. hibernates

하이버네이트는 자바 언어를 위한 ORM 프레임워크입니다. JPA의 구현체로, JPA 인터페이스를 구현하며, 내부적으로 JDBC API를 사용합니다.

SQL 중심 개발에서 객체 지향 중심적 개발

일반적인 SQLMapper 환경에서는, DB와의 상호작용을 위해 해당 SQL문을 따로 생성해야 하는 불편함이 있습니다. 이는 RDBMS의 테이블 연관 관계에 맞추어 application 내의 트랜잭션 로직을 설계해야 한다는 것입니다.

비록 RDBMS의 문법이 기본적으로 같다고 하지만, 각각의 제품마다 어느정도의 차이가 있기 때문에, application 단계에서의 코드가 해당 RDBMS의 문법을 고려해야 하며, 이는 JAVA환경이 특정 쿼리에 종속된다고 볼 수 있죠.
그러나 JPA에서는 그렇지가 않습니다. 어플리케이션 내에서의 POJO 객체만 신경쓰면 되니까요.

생산성

Hibernate는 SQL을 직접 사용하지 않고, 메서드 호출만으로 쿼리를 수행합니다. SQL 반복 작업을 줄입니다.

유지 보수

테이블 컬럼이 변경되었을 때, 테이블과 관련된 DAO의 파라미터, 결과, SQL 등을 대신 수행해줍니다. 이로 인해 유지보수 측면에서 용이합니다.

특정 벤더에 종속적이지 않음

JPA는 추상화된 데이터 접근 계층(Data Access Layer)를 제공합니다. 특정 벤더에 종속적이지 않습니다. 설정 파일에서 JPA에 어떤 DB를 사용하는지 등록만 하면 DB는 언제든지 바꿀 수 있습니다.

패러다임 불일치 해결

JPA는 annotation을 기반으로 객체 간 상속 관계를 테이블 간의 관계로 해석합니다. 이는 상속, 연관 관계, 객체 그래프 탐색, 비교 등 객체와 관계형 데이터베이스와의 패러다임 불일치를 하게 합니다.

성능

hibernates는 메서드 호출을 통해 쿼리를 수행 합니다. 이는 개발자의 의도와 상관 없이 자동적으로 수행된다는 것으로, 일정 수준의 SQL 쿼리문에 비해 성능적인 면에서 좋지 않습니다.

세밀함

메서드 호출만으로 DB 데이터를 조작하기에는 한계가 있습니다. 이는 JPQL을 통해 보완될 수 있습니다. 또한 NativeQuery를 지원하여 SQL 자체 쿼리도 작성할 수 있습니다.

러닝 커브

단순한 CRUD 처리에는 많은 이점을 보이지만, 트랜잭션에서의 성능 향상을 위한 엔티티 간의 관계 정립 등을 위해선 많은 학습 시간이 필요 합니다.

3. Data 유형에 대한 분류

DAO(Data Access Object) 는 데이터베이스의 data에 접근하기 위한 객체입니다. DataBase에 접근 하기 위한 로직 & 비지니스 로직을 분리하기 위해 사용합니다.

JPA에서는 모델을 다음과 같이 분류합니다.

3.1. DTO(Data Transfer Object)

계층(Layer)간 데이터 교환을 위해 사용하는 객체입니다.
데이터 교환만을 위해 사용하므로 로직을 갖지 않고, getter/setter 메소드만 갖고 있습니다.

3.2. VO(Value Object)

값 오브젝트로써 값을 위해 쓰입니다. read-Only 특징(사용하는 도중에 변경 불가능하며 오직 읽기만 가능)을 가집니다. DTO와 유사하지만 DTO는 setter를 가지고 있어 값이 변할 수 있습니다.

3.3. Entity

엔티티는 SQL의 table에 1:1로 대응하는 객체로서, 추상 데이터 모델(Conceptual Data Model, CDM) 상에서 쓰이는 '속성(attribute)'의 집합입니다.
SQL 혹은 DB 상에서 물리적으로 실존하는 '물리 모델링'인 테이블과 달리, 엔티티는 '추상 모델링'이라는 차이가 있습니다. JPA에서는 엔티티를 통해 DB에 영속성을 부여합니다.

3.4. entity와 table의 비교

논리 모델 물리 모델
엔티티(Entity) 테이블(Table)
속성, 어트리뷰트(Attribute) 컬럼(Column)
관계, 릴레이션(Relation) 관계, 릴레이션(Relation)
키 그룹(Key group) 인덱스(Index)


4. 코드 예시

4.1. Userinfo.java

@Data
@Entity(name = "userinfo")
@Table(name = "userinfo", 
        uniqueConstraints = {
                        @UniqueConstraint(name = "userinfo_id_unique", columnNames = {"id"})
})
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Userinfo implements UserDetails {

    @Id @GeneratedValue(generator = "uuid2")
    @GenericGenerator(name="uuid2", strategy = "uuid2")
    @Column(columnDefinition = "BINARY(16)")
    private UUID id;
    
    @Column(name = "email_id")
    private String emailId;

    @Column(name = "auth_id")   
    private String authId;

    @Column(name = "auth_type")
    private String authType;

    @Column(name = "withdraw")
    private Boolean withdraw;

    @Column(name = "withdraw_done")
    private Boolean withdrawDone;

    @Column(name = "login_rest")
    private Boolean loginRest;

    @Column(name = "password")
    private String password;

    @Column(name = "created_at")
    @CreationTimestamp
    private Date createdAt;
    
    @Column(name = "withdraw_date")
    private Date withdrawDate;

    @Column(name = "nickname_date")
    private Date nicknameDate;

    @Column(name = "login_date")
    @CurrentTimestamp(timing = GenerationTiming.ALWAYS)
    private Date loginDate;

    @Column(name = "login_rest_date")
    private Date loginRestDate;

    @Enumerated(EnumType.STRING)
    private Role role;

    @OneToMany(mappedBy = "userinfo")
    private List<AccessToken> accessTokens;
    
    @OneToMany(mappedBy = "userinfo")
    private List<RefreshToken> refreshTokens;

    @Column(name = "provider", length = 100)
    private String provider;
    
    //...    
}

entity객체인 Userinfo.java에서는 userDetails를 구현 받아 Spring Security에서 지원하는, 인증 절차에 대한 여러 구현 요소와 예외처리 등을 주입 받습니다.

@Entity의 경우, 프로그램이 실행 될 때 -설정에 따라- 해당 논리 모델에 대응하는 테이블이 DB에 자동적으로 생성됩니다. 그러나 테이블 내의 PK/FK 등의 설정들을 위해 @Table를 함께 annotation 선언을 했습니다.

@Id 해당 멤버 변수를 Id로 지정합니다.
@GenericGenerator를 통해 uuid 생성 전략을 주입 받고, @GneneratedValue를 통해, 해당 객체가 생성될 시, 자동적으로 변수가 생성됩니다.

@CurrentTimeStamp는 해당 Userinfo 객체가 생성될 시, 설정값에 따라 시간이 자동 생성됩니다.

@OneToMany 해당 Userinfo 객체에 발급 될 토큰 객체와의 관계를 설정합니다. 현재로서는 MySQL로 연관관계를 설정하기로 했습니다.

AuthenticationService.java

@Service
@RequiredArgsConstructor
@Slf4j
public class AuthenticationService {
	...
	    public AuthenticationResponse register(RegisterRequest request)  {
        var user = Userinfo.builder()
            .emailId(request.getEmail_id())
            .password(passwordEncoder.encode(request.getPassword()))
            .role(Role.USER)
            .withdraw(false)
            .build();

        userRepository.save(user);
	    	...
        
    }
  	...  
 }

해당 클래스에서 일부 발췌한 소스 코드 입니다.
컨트롤러를 통해 에서 DTO 파라미터인 request를 받은 AuthenticationService.register(request) 내부에서는 엔티티 객체인 user를 호출해 빌더 패턴으로 해당 프로세스에서 필요로 하는 파라미터 값을 선언합니다.
이 때 hibernates에서 지원하는 .save()는 해당 객체에 영속성 컨텍스트를 처리하여 Repository로 전달, DB에 저장 합니다.
builder()패턴을 사용할 경우, 엔티티 클래스의 생성자를 일일히 생성하거나, getter/setter를 통해 반복할 필요 없이 개발자의 편의에 따라 체이닝을 합니다.
(필요조건이 걸리지 않는 한) 파라미터 유무와는 상관없이 엔티티 그 자체로서 영속성을 부여할 수 있으므로, 일반적인 쿼리 조건문 등록에 비해 한결 작업이 수월합니다.
Java 11부터 지원된 타입추론을 통해 해당 객체는 var로 선언합니다.

4.2. UserService.java

@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class UserService {
    private final UserRepository userRepository;

    public Userinfo saveUser(Userinfo user){
        return userRepository.save(user);
    }

    public Optional<Userinfo> findByEmail(String email){
        return userRepository.findByEmail(email);
    }

    public List<Userinfo> findAllUsers(){
        return userRepository.findAll();
    }

    public Optional<Userinfo> getUserReferencedById(UUID id){
        return userRepository.getReferenceByUUID(id);
    }
}

hibernates에서는 쿼리 호출을 위한 메서드를 구현하기 위한 인터페이스를 제공합니다.
entity 통해 파라미터를 받은 영속성 컨테스트는 repository를 거쳐 DB에 영속성이 부여됩니다.

4.3. UserRepository.java

public interface UserRepository extends JpaRepository<Userinfo, Long>{

    @Query(value = "select * from userinfo where email_id = ?;",
        nativeQuery = true)
    public Optional<Userinfo> findByEmail(String email);

    @Query(value = "select * from userinfo where id = ?;",
        nativeQuery = true)
    public Optional<Userinfo> getReferenceByUUID(UUID id);

    @Query(value = "select * from userinfo where withdraw_date = ?;",
        nativeQuery = true)
    public List<Userinfo> isWithdraws(Date date);

    @Query(value = "select * from userinfo where email_id = ? , provider= ?;",
            nativeQuery = true)
    public Optional<Userinfo> findByEmailAndProvider(String emailId, String provider);

}

Repository에서 JPA를 사용하는 방법은 간단합니다. JpaRepository<>()를 해당 엔티티 모델에 구현하여 상속 하면 됩니다.
위에서 언급 했듯이, Hibernates에서는 해당 라이브러리에서 지원하는 메서드 호출 뿐 만 아니라, 필요에 의하면 NativeQuery 혹은 JPQL을 통해 쿼리문을 직접 작성하는 것도 가능합니다.

5. 레퍼런스


비문, 오탈자와 코드 오류 및 잘못된 지식에 대한 지적 및 질문은 언제나 환영합니다.

profile
프로개발자를 지망하는.

0개의 댓글