728x90
  • Entity 기본 annotation

@Entity: 엔티티 클래스에 설정, 필수!

@Table: mapping 테이블을 지정

@Id: 식별자 속성에 설정, 필수!

@Column: 매핑할 컬럼명 지정, 지정하지 않으면 필드명과 프로퍼티명을 사용

@Enumerated: enum 타입 매핑할 때 설정

 

  • Table annotation

annotation을 생략하면 클래스의 이름과 동일한 이름에 mapping

속성으로는

- name: 테이블 이름

- catalog: 카탈로그 이름

 

  • Enumerated

EnumType.STRING: enum 타입 값 이름을 저장

EnumType.ORDINAL: enum 타입의 값의 순서를 저장

 

 

Mapping이 된 설정의 예를 보도록 하자

//mapping을 합니다.
@Entity
//user 테이블에 mapping 합니다.
@Table(name = "user")
public class User {

	//식별자에 mapping 합니다.
    @Id
    //변수명에 mapping 합니다.
    private String email;
    private String name;
    
    //create_date에 mapping 합니다.
    @Column(name = "create_date")
    private LocalDateTime date;
	
    //열거타입 이름을 값으로 저장
    //grade에 mapping
    @Enumerated(EnumType.STRING)
    private Grade grade;

    protected User(){}
}

 

  • Entity 클래스의 제약 조건

1. 반드시 @Entity를 작성해야함

2. 반드시 @Id를 작성해야함

3. 인자 없는 기본 생성자 필요(해당 생성자는 public이나 protected로 작성이 되어야 함)

4. 최상위 클래스여야 함.

'Spring > JPA' 카테고리의 다른 글

JPA 6장 (@Embeddable)  (0) 2023.03.17
JPA 5장 (Entity 식별자 생성 방식)  (0) 2023.03.16
JPA 3장 (간단한 CRUD 구현해보기)  (0) 2023.03.16
JPA 2장 (영속 컨텍스트)  (0) 2023.03.15
JPA 1장 (우선 시작해보기)  (0) 2023.03.15
728x90

저번에 배웠던 CRUD를 사용해서 간단한 프로그램을 구현해보자.

 

package UserProgram;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;

public class EMF {

    private static EntityManagerFactory entityManagerFactory;

    public static void init(){
        entityManagerFactory = Persistence.createEntityManagerFactory("jpabegin");
    }

    public static EntityManager getEntityManager() {
        return entityManagerFactory.createEntityManager();
    }

    public static void close(){
        entityManagerFactory.close();
    }
}

어차피 EntityManagerFactory는 계속 사용을 해야하니, 초기화하고 EntityManagerFactory에서 EntityManager를 가져올 수 있는 class를 작성해준다.

 

당연히 간편하게 사용하기 위해 만드는 것으로 굳이 안 만들어도 되기는 한다.

 

그 다음에 DB에서 사용할 User 객체를 만들어준다.

저번에 사용했던 그대로 사용할 예정이다.

package UserProgram;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import java.time.LocalDateTime;

@Entity
@Table(name = "user")
public class User {

    @Id
    private String email;
    private String name;
    private LocalDateTime create_date;

    protected User(){}

    public User(String email, String name, LocalDateTime create_date) {
        this.email = email;
        this.name = name;
        this.create_date = create_date;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getName() {
        return name;
    }

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

    public LocalDateTime getCreate_data() {
        return create_date;
    }

    public void setCreate_data(LocalDateTime create_data) {
        this.create_date = create_data;
    }

    @Override
    public String toString() {
        return "User{" +
                "email='" + email + '\'' +
                ", name='" + name + '\'' +
                ", create_data=" + create_date +
                '}';
    }
}

이제 본격적으로 JPA를 사용할 차례이다.

Service 하는 메서드들을 한 곳에 모아 클래스로 작성했다.

 

package UserProgram;

import jakarta.persistence.Entity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityTransaction;

public class UserService {

    public void CreateUser(User user){
        EntityManager entityManager = EMF.getEntityManager();
        EntityTransaction entityTransaction = entityManager.getTransaction();

        try{
            entityTransaction.begin();
            entityManager.persist(user);
            entityTransaction.commit();
        }catch(Exception exception){
            entityTransaction.rollback();
            throw exception;
        } finally {
            entityManager.close();
        }
    }

    public User ReadUser(String email){
        EntityManager entityManager = EMF.getEntityManager();
        try{
            User user = entityManager.find(User.class, email);
            if(user == null) throw new NullPointerException();
            else return user;
        }finally{
            entityManager.close();
        }
    }

    public void UpdateUser(String email, String name){
        EntityManager entityManager = EMF.getEntityManager();
        EntityTransaction entityTransaction = entityManager.getTransaction();

        try{
            entityTransaction.begin();
            User user = entityManager.find(User.class, email);
            if(user == null) throw new NullPointerException();
            user.setName(name);
            entityTransaction.commit();
        }catch (Exception exception){
            entityTransaction.rollback();
            throw exception;
        }finally {
            entityManager.close();
        }
    }

    public void DeleteUser(String email){
        EntityManager entityManager = EMF.getEntityManager();
        EntityTransaction entityTransaction = entityManager.getTransaction();

        try{
            entityTransaction.begin();
            User user = entityManager.find(User.class, email);
            if (user == null) throw new NullPointerException();
            entityManager.remove(user);
            entityTransaction.commit();
        }catch(Exception exception){
            entityTransaction.rollback();
            throw exception;
        }finally {
            entityManager.close();
        }
    }
}

우선 저번에 배운 CRUD를 사용하였고, 다른 점이 있다면 static으로 작성된 EntityManagerFactory를 사용하였다.

우선 예외는 대충 넘기고 나중에 추가로 작성하도록 하겠다.

package UserProgram;

import java.time.LocalDateTime;
import java.util.Scanner;

public class UserProgram {

    private static UserService userService = new UserService();

    private static String email;
    private static String name;

    public static void main(String[] args) {
        EMF.init();

        Scanner scanner = new Scanner(System.in);

        boolean flag = true;

        while(flag){
            System.out.print("1. Create, 2. Read, 3. Update, 4. Delete, 5. exit : ");
            int answer = scanner.nextInt();
            switch(answer){
                case 1:
                    CreateMethod();
                    break;
                case 2:
                    ReadMethod();
                    break;
                case 3:
                    UpdateMethod();
                    break;
                case 4:
                    DeleteMethod();
                    break;
                case 5:
                    flag = false;
                    break;
            }
        }

        EMF.close();
        scanner.close();
    }

    private static void inputEmail(){
        Scanner scanner = new Scanner(System.in);
        System.out.print("input email:");
        email = scanner.nextLine();
    }

    private static void inputName(){
        Scanner scanner = new Scanner(System.in);
        System.out.print("input name:");
        name = scanner.nextLine();
    }

    private static void CreateMethod(){
        inputEmail();
        inputName();
        User user = new User(email, name, LocalDateTime.now());
        try{
            userService.CreateUser(user);
        }catch(Exception exception){
            System.out.println("createError");
        }
    }

    private static void ReadMethod(){
        inputEmail();
        try{
            User user = userService.ReadUser(email);
            System.out.println(user);
        }catch(Exception exception){
            System.out.println("readError");
        }
    }

    private static void UpdateMethod(){
        inputEmail();
        inputName();
        try{
            userService.UpdateUser(email, name);
            System.out.println(userService.ReadUser(email));
        }catch(Exception exception){
            System.out.println("updateError");
        }
    }

    private static void DeleteMethod(){
        inputEmail();
        try{
            userService.DeleteUser(email);
        }catch(Exception exception){
            System.out.println("deleteError");
        }
    }
}

이렇게 프로그램을 작성하였다.

그러면 이렇게 프로그램이 잘 작동한다.

 

값을 추가하고 삭제하는 메서드들도 잘 작동이 된다.

'Spring > JPA' 카테고리의 다른 글

JPA 6장 (@Embeddable)  (0) 2023.03.17
JPA 5장 (Entity 식별자 생성 방식)  (0) 2023.03.16
JPA 4장 (Entity에 대하여)  (0) 2023.03.16
JPA 2장 (영속 컨텍스트)  (0) 2023.03.15
JPA 1장 (우선 시작해보기)  (0) 2023.03.15
728x90

우선 코드 구조를 살펴보자.

import jakarta.persistence.*;
import jpabasic.reserve.domain.User;
import java.time.LocalDateTime;

public class UserSaveMain {
    public static void main(String[] args) {
        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpabegin");
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        EntityTransaction transaction = entityManager.getTransaction();

        try{
            transaction.begin();
            User user = new User("hello@gmail.com", "hello", LocalDateTime.now());
            entityManager.persist(user);
            transaction.commit();
        } catch(Exception exception){
            exception.printStackTrace();
            transaction.rollback();
        }finally {
            entityManager.close();
        }

        entityManagerFactory.close();
    }
}

데이터를 추가하는 코드는 저번에도 보았듯 이렇게 작성이 되었었다.

 

해당 코드 구조를 하나씩 살펴보면

EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpabegin");

EntityManagerFactory는 영속 단위 기준으로 생성을 한다.

Persistence.xml에 작성했던 영속 단위 기준이다.

애플리케이션을 처음 구동할 때 딱 한 번만 생성하게 된다.

그렇게 애플리케이션을 모두 사용하고 close 메서드를 이용하여 닫아서 자원을 반환한다.

 

EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction transaction = entityManager.getTransaction();

try{
	transaction.begin();
	User user = new User("hello@gmail.com", "hello", LocalDateTime.now());
	entityManager.persist(user);
	transaction.commit();
} catch(Exception exception){
	exception.printStackTrace();
	transaction.rollback();
}finally {
	entityManager.close();
}

EntityManager는 JPA에서 핵심 역할을 한다.

Factory를 이용해서 EntityManager를 생성하고

트랜젝션이 필요한 작업을 할 때는 entityTransaction을 구한다.

 

여기서 insert 쿼리는 commit을 하는 순간에 실행이 된다.

 

수정도 마찬가지로

transaction.begin();
User user = entityManager.find(User.class, "hello@gmail.com");
if(user == null) System.out.println("User 없음");
else user.setName("hi");

//수정도 커밋 시점에 update 쿼리를 실행
transaction.commit();

조회하는 순간에 select 쿼리가 실행이 되고  트랜젝션 사이에서 데이터를 변경하고 commit하는 순간에 update 쿼리를 실행한다.

 

이렇게 commit 시점에서 쿼리를 실행하는 게 영속 컨텍스트 때문이다.

영속 컨텍스트는 DB에서 읽어온 객체, 응용프로그램에서 저장한 객체, entityManager를 읽어온 객체를 저장하고 있는 메모리 공간이다.

이 객체들을 보관하고 있다가 commit 시점에 변경이 발생했는 지를 확인하고 그 변경된 내용을 DB에 반영한다.

그렇기 때문에 바로 DB에 저장이 되는 것이 아니라 영속 컨텍스트에서 저장되어 있다가 나중에 commit()을 실행하는 시점에 DB에 반영이 되는 것이다.

'Spring > JPA' 카테고리의 다른 글

JPA 6장 (@Embeddable)  (0) 2023.03.17
JPA 5장 (Entity 식별자 생성 방식)  (0) 2023.03.16
JPA 4장 (Entity에 대하여)  (0) 2023.03.16
JPA 3장 (간단한 CRUD 구현해보기)  (0) 2023.03.16
JPA 1장 (우선 시작해보기)  (0) 2023.03.15
728x90

일단 바로 만들어보고 시작하자.

 

intellij에서 JDK17로 Maven 프로젝트를 생성한다.

그리고 pom.xml을 다음과 같이 작성한다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>jpa-basic</groupId>
    <artifactId>JPA-01</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <hibernate.verstion>6.0.0.FINAL</hibernate.verstion>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>${hibernate.verstion}</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-hikaricp</artifactId>
            <version>${hibernate.verstion}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.11</version>
        </dependency>
    </dependencies>

</project>

우선 이렇게 작성해주고 src/main/resources/META-INF에 가서는 persistence.xml을 만든 후

<?xml version="1.0" encoding="utf-8" ?>

<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence
                https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
             version="3.0">

    <persistence-unit name="jpabegin" transaction-type="RESOURCE_LOCAL">
        <class>jpabasic.reserve.domain.User</class>
        <exclude-unlisted-classes>true</exclude-unlisted-classes>
        <properties>
            <property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
            <property name="jakarta.persistence.jdbc.url"
                      value="jdbc:mysql://localhost/jpabegin?characterEncoding=utf8"/>
            <property name="jakarta.persistence.jdbc.user" value="jpauser"/>
            <property name="jakarta.persistence.jdbc.password" value="12345678"/>

            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
            <property name="hibernate.hikari.poolName" value="pool"/>
            <property name="hibernate.hikari.maximumPoolSize" value="10"/>
            <property name="hibernate.hikari.minimumIdle" value="10"/>
            <property name="hibernate.hikari.connectionTimeout" value="1000"/>
        </properties>
    </persistence-unit>
</persistence>

이렇게 작성을 해준다.

MySQL을 사용하고 사용자 정보를 저렇게 사용하기 때문에 저렇게 만든 것이다.

 

이 상태로 바로 class를 작성하러 가보자.

JDBC를 공부했으니 어느정도는 이해가 될 수도 있다.

package jpabasic.reserve.domain;

import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "user")
public class User {
    @Id
    private String email;
    private String name;
    private String create_date;

    public User(String email, String name, String create_date) {
        this.email = email;
        this.name = name;
        this.create_date = create_date;
    }

    protected User() {
        
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getName() {
        return name;
    }

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

    public String getCreate_date() {
        return create_date;
    }

    public void setCreate_date(String create_date) {
        this.create_date = create_date;
    }
}

클래스 위에 @Entity annotation으로 요소임을 알리고, @Table로 테이블을 지정하며, @Id로 식별자들을 지정해준다.

그리고 당연히 생성자와 Getter, Setter를 만들어준다.

 

import jakarta.persistence.*;
import jpabasic.reserve.domain.User;
import java.time.LocalDateTime;

public class UserSaveMain {
    public static void main(String[] args) {
        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpabegin");
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        EntityTransaction transaction = entityManager.getTransaction();

        try{
            transaction.begin();
            User user = new User("hello@gmail.com", "hello", LocalDateTime.now());
            entityManager.persist(user);
            transaction.commit();
        } catch(Exception exception){
            exception.printStackTrace();
            transaction.rollback();
        }finally {
            entityManager.close();
        }

        entityManager.close();
    }
}

 

우선 EntityManagerFactory를 생성한다.

이 때 xml에서 작성했던 jpabegin 이름을 이용한다.

 

persist를 사용하면 parameter로 넘긴 객체가 DB에 저장이 된다.

실행을 해보면 잘 들어가는 것을 볼 수 있다.

당연히 한 번 더 추가하면 pk가 겹치기 때문에 추가할 수 없다고 나온다.

 

이번엔 조회다.

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;
import jpabasic.reserve.domain.User;

public class UserGetMain {
    public static void main(String[] args) {

        String find_id = "hello@gmail.com";

        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpabegin");

        EntityManager entityManager = entityManagerFactory.createEntityManager();
        EntityTransaction transaction = entityManager.getTransaction();

        try{
            transaction.begin();
            User user = entityManager.find(User.class, find_id);
            if (user == null) System.out.println("User 없음");
            else {
                System.out.printf("email : %s, name = %s, createDate = %s\n", user.getEmail(), user.getName(), user.getCreate_date());
            }
            transaction.commit();
        }catch (Exception exception){
            exception.printStackTrace();
            transaction.rollback();
        }finally {
            entityManager.close();
        }
        entityManager.close();
    }
}

entityManager에서 find로 찾아온다.

parameter는 해당 클래스와 PK이다.

실행을 해보면 정상적으로 조회되는 것을 볼 수 있다.

 

이번엔 수정이다.

import jakarta.persistence.*;
import jpabasic.reserve.domain.User;
import java.time.LocalDateTime;

public class UserUpdateMain {
    public static void main(String[] args) {
        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpabegin");
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        EntityTransaction transaction = entityManager.getTransaction();

        try{
            transaction.begin();
            User user = entityManager.find(User.class, "hello@gmail.com");
            if(user == null) System.out.println("User 없음");
            else{
                user.setName("hi");
            }
            transaction.commit();
        } catch(Exception exception){
            exception.printStackTrace();
            transaction.rollback();
        }finally {
            entityManager.close();
        }

        entityManager.close();
    }
}

수정은 조회를 한 후 해당 클래스의 데이터를 변경하고 commit을 수행하면 된다.

transaction 범위 내에서 수정해야 한다.

이렇게 정상적으로 수행되는 것을 볼 수 있다.

'Spring > JPA' 카테고리의 다른 글

JPA 6장 (@Embeddable)  (0) 2023.03.17
JPA 5장 (Entity 식별자 생성 방식)  (0) 2023.03.16
JPA 4장 (Entity에 대하여)  (0) 2023.03.16
JPA 3장 (간단한 CRUD 구현해보기)  (0) 2023.03.16
JPA 2장 (영속 컨텍스트)  (0) 2023.03.15
728x90

이제 JDBC를 이용해서 데이터를 다뤄보자.

 

  • 데이터 수정하기

우선 MySQL로 가보자.

NOTICE 테이블에서

ID와 REGDATE, HIT는 사용자가 입력하는 정보는 아니다.

REGDATE는 현재 시각을 넣고, HIT는 조회할 때마다 하나씩 증가하며 ID는 1부터 시작해서 하나씩 증가할 것이다.

그렇기 때문에 일단 ID에 자동 증가를 설정해두고, HIT에 디폴트를 0으로 설정해준다.

 

그 후에 테스트로

INSERT INTO NOTICE(
    TITLE,
    WRITER_ID,
    CONTENT,
    REGDATE,
    FILES
) VALUES (
	"SEUNGKYU",
    "SK",
    "AAA",
    NOW(),
    ""
);

해당 값을 넣은 후 데이터가 잘 들어가는 지 테스트 해본다.

이렇게 잘 들어간다.

auto_increment를 다시 1로 설정해주고 JDBC로 넘어간다.

 

Program1의 코드를 그대로 복사해서 Program2를 만들었다.

기본적인 내용들은 크게 변하지는 statement를 PreparedStatement, executeQuery를 executeUpdate로 변경해주어야 한다.

해당 코드들은 조회에서만 사용하는 코드들이다.

 

executeUpdate는 조회가 아니가 때문에 결과 집합을 반환하지 않고, 업데이트 된 데이터의 개수를 반환한다.

 

우선 sql 문장부터 만들어 본다면

import java.sql.*;

public class Program2 {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {

        String driver = "com.mysql.cj.jdbc.Driver";
        String url = "jdbc:mysql://localhost:3306/jdbc";
        String user = "root";
        String passwd = "12345678";

        String title = "seungkyubook";
        String writer_id = "sk";
        String content = "aaa";
        String files = "";

        String sql = "INSERT INTO NOTICE (TITLE, WRITER_ID, CONTENT, REGDATE, FILES) VALUES ()";



        Class.forName(driver);

        Connection connection = DriverManager.getConnection(url, user, passwd);

        Statement statement = connection.createStatement();

        int result = statement.executeUpdate(sql);
        
        
        statement.close();
        connection.close();
    }
}

 

저렇게 만든 후 VALUES 안에 우리가 넣고 싶어하느 데이터들을 +로 연결해주어야 했다.

이렇게 이어 붙이기 위해서는 문자열에는 '를 또 달아주기도 해야한다.

너무 귀찮고 실수도 많이 일어나는 부분이다.

 

다행이게도 JDBC에서 이 부분을 도와준다.

방법은

import java.sql.*;

public class Program2 {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {

        String driver = "com.mysql.cj.jdbc.Driver";
        String url = "jdbc:mysql://localhost:3306/jdbc";
        String user = "root";
        String passwd = "12345678";

        String title = "seungkyubook";
        String writer_id = "sk";
        String content = "aaa";
        String files = "";

        String sql = "INSERT INTO NOTICE (TITLE, WRITER_ID, CONTENT, REGDATE, FILES) VALUES (?, ?, ?, NOW(),?)";

        Class.forName(driver);

        Connection connection = DriverManager.getConnection(url, user, passwd);

        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        preparedStatement.close();
        connection.close();
    }
}

이렇게 문자열이 추가될 부분을 그냥 ?로 놔둔다.

이 부분들을 PreparedStatement가 해결해준다.

실행할 때 sql을 넘기는 것이 아니라 미리 준비해 둔다는 말이다.

 

        preparedStatement.setString(1, title);
        preparedStatement.setString(2, writer_id);
        preparedStatement.setString(3, content);
        preparedStatement.setString(4, files);

그러고 PreparedStatement의 set???(index, data)로 물음표를 채워준다.

여기서 조심할 점은 index가 0부터 시작하는 것이 아니라 1부터 시작한다는 점이다.

당연히 정수값을 넣으려면 setInt() 이런 느낌이다.

int result = preparedStatement.executeUpdate();

문장만 준비해두고 executeUpdate를 실행하면 업데이트가 된다.

이미 sql을 준비해두었기 때문에 sql을 넘기지 않는다.

import java.sql.*;

public class Program2 {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {

        String driver = "com.mysql.cj.jdbc.Driver";
        String url = "jdbc:mysql://localhost:3306/jdbc";
        String user = "root";
        String passwd = "12345678";

        String title = "seungkyubook";
        String writer_id = "sk";
        String content = "aaa";
        String files = "";

        String sql = "INSERT INTO NOTICE (TITLE, WRITER_ID, CONTENT, REGDATE, FILES) VALUES (?, ?, ?, NOW(),?)";

        Class.forName(driver);

        Connection connection = DriverManager.getConnection(url, user, passwd);

        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        preparedStatement.setString(1, title);
        preparedStatement.setString(2, writer_id);
        preparedStatement.setString(3, content);
        preparedStatement.setString(4, files);

        int result = preparedStatement.executeUpdate();

        Statement statement = connection.createStatement();

        ResultSet resultSet = statement.executeQuery("SELECT * FROM NOTICE");

        while(resultSet.next()){
            int result_id = resultSet.getInt("ID");
            String result_title = resultSet.getString("TITLE");
            String result_writer_id = resultSet.getString("WRITER_ID");
            String result_content = resultSet.getString("CONTENT");
            Date result_regdate = resultSet.getDate("REGDATE");
            int result_hit = resultSet.getInt("HIT");
            String result_files = resultSet.getString("FILES");

            System.out.printf("id: %d, title: %s, write_id: %s, content: %s, regDate: %s, hit: %d, files: %s\n",
                    result_id, result_title, result_writer_id, result_content, result_regdate, result_hit, result_files);
        }

        preparedStatement.close();
        connection.close();
    }
}

최종적으로 작성한 코드이다.

데이터를 추가하고 바로 조회할 수 있도록 작성해보았다.

 

실행을 해보면

이렇게 잘 추가되는 것을 볼 수 있다.

 

  • 데이터 수정하기

수정은 그냥 PreparedStatement를 수정에 맞게 수정하면 된다.

import java.sql.*;

public class Program3 {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {

        String driver = "com.mysql.cj.jdbc.Driver";
        String url = "jdbc:mysql://localhost:3306/jdbc";
        String user = "root";
        String passwd = "12345678";

        int id = 6;
        String title = "seungkyu!!!";
        String writer_id = "sk";
        String content = "aaa";
        String files = "";

        String sql = "UPDATE NOTICE SET TITLE = ?, WRITER_ID = ?, CONTENT = ?, REGDATE = NOW(), FILES = ? WHERE ID = ?";

        Class.forName(driver);

        Connection connection = DriverManager.getConnection(url, user, passwd);

        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        preparedStatement.setString(1, title);
        preparedStatement.setString(2, writer_id);
        preparedStatement.setString(3, content);
        preparedStatement.setString(4, files);
        preparedStatement.setInt(5, id);

        int result = preparedStatement.executeUpdate();

        Statement statement = connection.createStatement();

        ResultSet resultSet = statement.executeQuery("SELECT * FROM NOTICE");

        while(resultSet.next()){
            int result_id = resultSet.getInt("ID");
            String result_title = resultSet.getString("TITLE");
            String result_writer_id = resultSet.getString("WRITER_ID");
            String result_content = resultSet.getString("CONTENT");
            Date result_regdate = resultSet.getDate("REGDATE");
            int result_hit = resultSet.getInt("HIT");
            String result_files = resultSet.getString("FILES");

            System.out.printf("id: %d, title: %s, write_id: %s, content: %s, regDate: %s, hit: %d, files: %s\n",
                    result_id, result_title, result_writer_id, result_content, result_regdate, result_hit, result_files);
        }

        preparedStatement.close();
        connection.close();
    }
}

 

  • 데이터 삭제하기
import java.sql.*;

public class Program4 {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {

        String driver = "com.mysql.cj.jdbc.Driver";
        String url = "jdbc:mysql://localhost:3306/jdbc";
        String user = "root";
        String passwd = "12345678";

        int id = 6;

        String sql = "DELETE FROM NOTICE WHERE id = ?";

        Class.forName(driver);

        Connection connection = DriverManager.getConnection(url, user, passwd);

        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        preparedStatement.setInt(1, id);


        preparedStatement.executeUpdate();

        Statement statement = connection.createStatement();

        ResultSet resultSet = statement.executeQuery("SELECT * FROM NOTICE");

        while(resultSet.next()){
            int result_id = resultSet.getInt("ID");
            String result_title = resultSet.getString("TITLE");
            String result_writer_id = resultSet.getString("WRITER_ID");
            String result_content = resultSet.getString("CONTENT");
            Date result_regdate = resultSet.getDate("REGDATE");
            int result_hit = resultSet.getInt("HIT");
            String result_files = resultSet.getString("FILES");

            System.out.printf("id: %d, title: %s, write_id: %s, content: %s, regDate: %s, hit: %d, files: %s\n",
                    result_id, result_title, result_writer_id, result_content, result_regdate, result_hit, result_files);
        }

        preparedStatement.close();
        connection.close();
    }
}

늘 그렇듯이 DELETE로 바꾸어주면 된다.

이렇게 모든 데이터가 지워진 것을 볼 수 있다.

 

  • CRUD 서비스 구현

배웠던 조회, 입력, 수정, 삭제를 이용하여 서비스를 만들어보자.

 

NOTICE를 대상으로 하기 때문에 패키지를 만들고 진행할 예정이다.

우선 데이터를 가져오고 저장할 NOTICE 클래스를 만들어보자.

package Service;

import java.util.Date;

public class Notice {
    private int id;
    private String title;
    private String writerId;
    private String content;
    private Date regDate;
    private int hit;
    private String files;

    public Notice(int id, String title, String writerId, String content, Date regDate, int hit, String files) {
        this.id = id;
        this.title = title;
        this.writerId = writerId;
        this.content = content;
        this.regDate = regDate;
        this.hit = hit;
        this.files = files;
    }

    public int getId() {
        return id;
    }

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

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getWriterId() {
        return writerId;
    }

    public void setWriterId(String writerId) {
        this.writerId = writerId;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public Date getRegDate() {
        return regDate;
    }

    public void setRegDate(Date regDate) {
        this.regDate = regDate;
    }

    public int getHit() {
        return hit;
    }

    public void setHit(int hit) {
        this.hit = hit;
    }

    public String getFiles() {
        return files;
    }

    public void setFiles(String files) {
        this.files = files;
    }
}

당연히 Private로 멤버들을 선언해주고, Getter와 Setter를 만들어준다.

 

전에 만들었던 조회 부분을 살짝 수정해서 getList를 완성해주었다.

package Service;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class NoticeService {

    String driver = "com.mysql.cj.jdbc.Driver";
    String url = "jdbc:mysql://localhost:3306/jdbc";
    String user = "root";
    String passwd = "12345678";

    public List<Notice> getList() throws ClassNotFoundException, SQLException {

        List<Notice> list = new ArrayList<Notice>();

        Class.forName(driver);

        Connection connection = DriverManager.getConnection(url, user, passwd);

        Statement statement = connection.createStatement();

        ResultSet resultSet = statement.executeQuery("SELECT * FROM NOTICE");

        while(resultSet.next()){
            int id = resultSet.getInt("ID");
            String title = resultSet.getString("TITLE");
            String writer_id = resultSet.getString("WRITER_ID");
            Date regDate = resultSet.getDate("REGDATE");
            String content = resultSet.getString("CONTENT");
            int hit = resultSet.getInt("HIT");
            String files = resultSet.getString("FILES");

            Notice notice = new Notice(id, title, writer_id, content, regDate, hit, files);

            list.add(notice);
        }
        resultSet.close();
        statement.close();
        connection.close();

        return list;
    }
}

다른 파일들도 복사한 후 코드를 약간 수정해서 메서드들을 작성했다.

package Service;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class NoticeService {

    String driver = "com.mysql.cj.jdbc.Driver";
    String url = "jdbc:mysql://localhost:3306/jdbc";
    String user = "root";
    String passwd = "12345678";

    public List<Notice> getList() throws ClassNotFoundException, SQLException {

        List<Notice> list = new ArrayList<>();

        Class.forName(driver);

        Connection connection = DriverManager.getConnection(url, user, passwd);

        Statement statement = connection.createStatement();

        ResultSet resultSet = statement.executeQuery("SELECT * FROM NOTICE");

        while(resultSet.next()){
            int id = resultSet.getInt("ID");
            String title = resultSet.getString("TITLE");
            String writer_id = resultSet.getString("WRITER_ID");
            Date regDate = resultSet.getDate("REGDATE");
            String content = resultSet.getString("CONTENT");
            int hit = resultSet.getInt("HIT");
            String files = resultSet.getString("FILES");

            Notice notice = new Notice(id, title, writer_id, content, regDate, hit, files);

            list.add(notice);
        }
        resultSet.close();
        statement.close();
        connection.close();

        return list;
    }

    public int insert(Notice notice) throws ClassNotFoundException, SQLException {
        String sql = "INSERT INTO NOTICE (TITLE, WRITER_ID, CONTENT, REGDATE, FILES) VALUES (?, ?, ?, NOW(),?)";

        String title = notice.getTitle();
        String writerId = notice.getWriterId();
        String content = notice.getContent();
        String files = notice.getFiles();

        Class.forName(driver);

        Connection connection = DriverManager.getConnection(url, user, passwd);

        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        preparedStatement.setString(1, title);
        preparedStatement.setString(2, writerId);
        preparedStatement.setString(3, content);
        preparedStatement.setString(4, files);

        int result = preparedStatement.executeUpdate();


        preparedStatement.close();
        connection.close();

        return result;
    }

    public int update(Notice notice) throws ClassNotFoundException, SQLException {
        String sql = "UPDATE NOTICE SET TITLE = ?, WRITER_ID = ?, CONTENT = ?, REGDATE = NOW(), FILES = ? WHERE ID = ?";

        int id = notice.getId();
        String title = notice.getTitle();
        String writerId = notice.getWriterId();
        String content = notice.getContent();
        String files = notice.getFiles();

        Class.forName(driver);

        Connection connection = DriverManager.getConnection(url, user, passwd);

        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        preparedStatement.setString(1, title);
        preparedStatement.setString(2, writerId);
        preparedStatement.setString(3, content);
        preparedStatement.setString(4, files);
        preparedStatement.setInt(5, id);

        int result = preparedStatement.executeUpdate();

        preparedStatement.close();
        connection.close();

        return result;
    }

    public int delete(int id) throws ClassNotFoundException, SQLException {
        String sql = "DELETE FROM NOTICE WHERE id = ?";

        Class.forName(driver);

        Connection connection = DriverManager.getConnection(url, user, passwd);
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setInt(1, id);

        int result = preparedStatement.executeUpdate();

        Statement statement = connection.createStatement();

        preparedStatement.close();
        connection.close();

        return result;
    }
}

'Spring > JDBC' 카테고리의 다른 글

JDBC 1일차  (0) 2023.03.12
728x90
  • JDBC란 무엇인가?

JDBC(Java Database Connectivity)는 자바에서 DB에 접속할 수 있도록 하는 자바의 API이다. JDBC는 데이터베이스에서 자료를 쿼리하거나 업데이트하는 방법을 제공한다.

자바와 DBMS 사이에서 통역을 해주는 통역사의 역할을 수행한다.

JDBC를 사용하면 개발자는 DBMS를 신경쓰지 않고 JDBC만을 사용해서 개발을 할 수 있게 된다.

JDBC를 사용하기 위해서는 해당 DBMS의 JDBC 드라이버를 사용해야 한다.

 

 

  • JDBC Driver

mac을 사용하고 있기 때문에

 

https://dev.mysql.com/downloads/connector/j/

 

MySQL :: Download Connector/J

MySQL Connector/J 8.0 is highly recommended for use with MySQL Server 8.0, 5.7 and 5.6. Please upgrade to MySQL Connector/J 8.0.

dev.mysql.com

해당 사이트에서

Platform Independent를 선택해서 다운 받아준다.

 

다운로드 받은 후 압축을 풀어둔다.

 

이제 실습을 하기 위한 Intellij를 열어주자.

새로운 프로젝트를 생성하고 Project Structure에서 다운로드 받고 압축을 해제했던 connector를 추가해준다.

OK를 누르면 이제 JDBC를 사용할 준비가 된 것이다.

 

  • JDBC 기본코드

우선은 MySQL에서 받은 드라이버를 로드해야 한다.

 

그렇기에 드라이버를 로드하는 것이 가장 첫번째 코드이다.

Class.forName("com.mysql.cj.jdbc.driver");

이 코드로 MySQL 드라이버를 로드하자.

 

이렇게 드라이버를 로드하면 메모리에 잡히게 된다.

 

이 드라이버를 연결해 연결 객체를 얻어야 한다.

Connection connection = DriverManager.getConnection(...);

현재 로컬에 있는 데이터베이스에 연결할 것이기 때문에

Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/DB명", "user", "password");

이렇게 연결하도록 하자.

 

실행도구를 생성한다.

Statement statement = connection.createStatement();

 

쿼리를 실행하고 결과를 받아온다.

ResultSet resultSet = statement.executeQuery(sql);

 

현재 ResultSet은 파일의 시작(Before of File)을 가르키고 있으며, 이 ResultSet은 ResultSet.next를 이용하여 다음 정보들을 넘겨 받게 된다.

그러다가 파일의 마지막(End of File)을 가르키면 더 이상 데이터를 반환하지 않게 된다.

 

ResultSet.getXXX("열이름") 메서드를 사용하면 현재 ResultSet이 가르키고 있는 행의 열 이름에 해당하는 정보를 가져오게 된다.

 

바로 실습을 해보도록 하자.

참고로 이 부분에 대한 코드는 거의 고정이니 외워서 사용하도록 하자.

Class.forName("com.mysql.cj.jdbc.Driver");

Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc", "root", "12345678");

Statement statement = connection.createStatement();

ResultSet resultSet = statement.executeQuery("");
        
//
//
        
resultSet.close();
statement.close();
connection.close();

우선 DB에서 데이터를 가져오기 위해 데이터를 추가한다.

 

INSERT INTO NOTICE VALUES
(
    1,
    "hello",
    "sk",
    "aaa",
    SYSDATE(),
    0,
    ""
);

이렇게 NOTICE 테이블에 데이터를 대충 추가한 후

 

import java.sql.*;

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {

        Class.forName("com.mysql.cj.jdbc.Driver");

        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc", "root", "12345678");

        Statement statement = connection.createStatement();

        ResultSet resultSet = statement.executeQuery("SELECT * FROM NOTICE");



        if(resultSet.next()){
            String title = resultSet.getString("TITLE");
            System.out.println(title);
        }

        resultSet.close();
        statement.close();
        connection.close();
    }
}

ResultSet에서 다음 데이터를 가져온 후 title 열의 데이터를 출력해보면

 

이렇게 잘 출력이 되는 것을 볼 수 있다.

 

데이터를 하나 더 추가하여 while 문으로 모든 데이터를 출력한 모습이다.

당연히 MySQL과 마찬가지로 쿼리에 WHERE 문도 사용 할 수가 있다.

import java.sql.*;

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {

        Class.forName("com.mysql.cj.jdbc.Driver");

        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc", "root", "12345678");

        Statement statement = connection.createStatement();

        ResultSet resultSet = statement.executeQuery("SELECT * FROM NOTICE WHERE ID > 1");

        while(resultSet.next()){
            int id = resultSet.getInt("ID");
            String title = resultSet.getString("TITLE");
            String writer_id = resultSet.getString("WRITER_ID");
            Date regDate = resultSet.getDate("REGDATE");
            String content = resultSet.getString("CONTENT");
            int hit = resultSet.getInt("HIT");

            System.out.printf("id: %d, title: %s, write_id: %s, regDate: %s, content: %s, hit: %d\n", id, title, writer_id, regDate, content, hit);
        }

        resultSet.close();
        statement.close();
        connection.close();
    }
}

'Spring > JDBC' 카테고리의 다른 글

JDBC 2일차  (0) 2023.03.14
728x90

인프런 김영한 님의 강의를 참고했습니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

빈 스코프에 대해서 배워보자.

 

11일차에 우리는 스프링 빈이 컨테이너가 시작될 때 생성이되어 컨테이너가 종료될 때까지 유지된다고 배웠다.

싱글톤 스코프로 생성이 되기 때문인데, 스코프는 말 그대로 빈이 존재할 수 있는 범위이다.

 

스프링은 다음과 같은 스코프를 지원한다.

  • 싱글톤

기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.

  • 프로토타입

스프링 컨테이너는 프로토타입 빈의 생성과 의존관계까지만 관여하고 더는 관리하지 않는 짧은 범위의 스코프이다.

  • 웹 관련 스코프

웹 요청에 관한 스코프들로 request, session, application이 있다.

 

 

빈 스코프는

@Scope("prototype")로 설정할 수 있다.

 

프로토타입 스코프

기존의 싱글톤을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환했다.

하지만 프로토타입 스코프를 조회하면 항상 새로운 인스턴스를 생성해서 반환해준다.

 

 

프로토타입 스코프의 빈을 스프링 컨테이너에 요청하면

스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.

그 빈을 스프링 컨테이너는 반환한다.

 

스프링은 딱 이 과정에만 관여하고, 그 후에는 관리를 하지 않는다.

따라서 @PreDestroy와 같은 종료 메서드도 호출되지 않는다.

 

늘 하던대로 싱글톤 스코프 빈을 먼저 테스트 해보자

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class SingletonTest {
    
    @Scope("singleton")
    static class SingletonBean{
        @PostConstruct
        public void init(){
            System.out.println("SingletonBean.init");
        }
        
        @PreDestroy
        public void destroy(){
            System.out.println("SingletonBean.destroy");
        }
    }
    
    @Test
    public void singletonBeanFind(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(SingletonBean.class);
        
        SingletonBean singletonBean1 = applicationContext.getBean(SingletonBean.class);
        SingletonBean singletonBean2 = applicationContext.getBean(SingletonBean.class);

        System.out.println("singletonBean1 = " + singletonBean1);
        System.out.println("singletonBean2 = " + singletonBean2);

        Assertions.assertSame(singletonBean1, singletonBean2);
        
        applicationContext.close();
    }
}

이렇게 같은 인스턴스가 출력이 되는 것을 볼 수 있다.

 

이번에는 싱글톤이 아닌 프로토타입을 테스트해보자.

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class PrototypeTest {

    @Scope("prototype")
    static class PrototypeBean{
        @PostConstruct
        public void init(){
            System.out.println("PrototypeBean.init");
        }

        @PreDestroy
        public void destroy(){
            System.out.println("PrototypeBean.destroy");
        }
    }

    @Test
    public void prototypeBeanFind(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(PrototypeBean.class);
        System.out.println("find prototypeBean1");
        PrototypeBean prototypeBean1 = applicationContext.getBean(PrototypeBean.class);

        System.out.println("find prototypeBean2");
        PrototypeBean prototypeBean2 = applicationContext.getBean(PrototypeBean.class);

        System.out.println("prototypeBean1 = " + prototypeBean1);
        System.out.println("prototypeBean2 = " + prototypeBean2);

        Assertions.assertNotSame(prototypeBean1, prototypeBean2);
        applicationContext.close();
    }
}

이렇게 프로토타입을 테스트해보면

다른 인스턴스가 생성이 되는 것을 확인할 수 있다.

그리고 초기화도 2번이 실행된 것을 확인할 수 있다.

또한 프로토타입은 생성, 의존관계주입, 초기화까지만 관여하기에 @PreDestroy의 메서드가 출력되지 않는 것을 볼 수 있다.

 

프로토타입과 싱글톤을 함께 사용시의 문제점

프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 반환한다.

하지만 싱글톤 빈에 들어간 프로토타입 빈은 어떻게 될까?

 

싱글톤 빈이 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈일 것이다.

그렇기 때문에 사용할 때마다 생성되는 것이 아니며, 안에 프로토타입 빈이 있더라도 프로토타입 빈으로의 작동을 못하게 될 것이다.

이렇게 사용할 수도 있지만, 아마 이것은 프로토타입을 사용하는 취지에 어긋날 것이다.

그렇기에 항상 새로운 프로토타입 빈을 생성하는 방법에 대해 알아보자.

 

Provider로 프로토타입 스코프를 사용

프로토타입 빈을 요청받는 방법은 사용할 때마다 스프링 컨테이너에 새로 요청하는 것이다.

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class PrototypeProviderTest {

    @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");
        }
    }

    static class ClientBean{
        @Autowired
        private ApplicationContext applicationContext;

        public int logic(){
            PrototypeBean prototypeBean = applicationContext.getBean(PrototypeBean.class);
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

    @Test
    void providerTest(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean clientBean1 = applicationContext.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        Assertions.assertEquals(1, count1);

        ClientBean clientBean2 = applicationContext.getBean(ClientBean.class);
        int count2 = clientBean1.logic();
        Assertions.assertEquals(1, count2);
    }
}

이렇게 의존관계를 찾는 방법을 Dependency Lookup이라고 하는데, 스프링 컨테이너 종속적인 코드가 되고 좋은 코드가 아니다.

 

따라서 다른 방법들을 사용한다.

 

ObjectFactory, ObjectProvider

Dependency Lookup 서비스를 제공하는 것이 바로 ObjectProvider이고, 이걸 상속받아 기능을 추가한 것이 ObjectProvider이다.

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class PrototypeProviderTest {

    @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");
        }
    }

    static class ClientBean{
        @Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanObjectProvider;
        
        public int logic(){
            PrototypeBean prototypeBean = prototypeBeanObjectProvider.getObject();
            prototypeBean.addCount();
            return prototypeBean.getCount();
        }
    }

    @Test
    void providerTest(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean clientBean1 = applicationContext.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        Assertions.assertEquals(1, count1);

        ClientBean clientBean2 = applicationContext.getBean(ClientBean.class);
        int count2 = clientBean1.logic();
        Assertions.assertEquals(1, count2);
    }
}

ClientBean 클래스의 내용을 이렇게 변경하면 된다.

이렇게 다른 인스턴스를 가지는 것을 볼 수 있다.

ObjectProvider의 getObject()를 호출하면 스프링 컨테이너에서 해당 빈을 찾아서 반환한다.

 

여기에 추가로 자바 표준을 사용하는 방법이 있다.

 

JSP-330 Provider

스프링 부트 3.0.1을 사용하는 방법이다.

우선 gradle에

implementation 'jakarta.inject:jakarta.inject-api:2.0.1'

을 추가해준다.

 

그리고 Provider에 맞게 ClientBean의 코드를 수정해준다.

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Provider;
import org.junit.jupiter.api.Assertions;
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;

public class SingletonWithPrototypeTest1 {

    @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");
        }
    }

    static class ClientBean{
        
        @Autowired
        private Provider<PrototypeBean> provider;
        
        public int logic(){
            PrototypeBean prototypeBean = provider.get();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

    @Test
    void prototypeFind(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(PrototypeBean.class);
        PrototypeBean prototypeBean1 = applicationContext.getBean(PrototypeBean.class);
        prototypeBean1.addCount();
        Assertions.assertEquals(1, prototypeBean1.getCount());

        PrototypeBean prototypeBean2 = applicationContext.getBean(PrototypeBean.class);
        prototypeBean2.addCount();
        Assertions.assertEquals(1, prototypeBean2.getCount());
    }

    @Test
    void singletonClientUsePrototype(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean clientBean1 = applicationContext.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        Assertions.assertEquals(1, count1);

        ClientBean clientBean2 = applicationContext.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        Assertions.assertEquals(2, count2);
    }
}

둘 중에 상황에 맞는 DL을 사용하면 된다.

 

웹 스코프

지금까지는 싱글톤과 프로토타입 스코프를 학습했다.

 

이번에는 웹 스코프에 대해서도 알아보자.

웹 스코프는 웹 환경에서만 동작한다.

그리고 프로토타입과는 다르게 해당 스코프의 종료시점까지 관리해준다.

 

종류로는

request: HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프, 요청마다 별도의 인스턴스가 생성된다.

session: HTTP Session과 동일한 생명주기를 가지는 스코프

application: ServletContext와 동일한 생명주기를 가지는 스코프

 

바로 request 스코프 예제를 만들어보자.

웹 스코프는 웹 환경에서만 동작하기 때문에 해당 라이브러리를 추가해줘야 한다.

implementation 'org.springframework.boot:spring-boot-starter-web'

해당 라이브러리를 추가하면

http://localhost:8080/ 해당 페이지를 통해 접속할 수 있게 된다.

 

request 요청이 오면 로그를 남기는 예제를 만들어보자.

우선 로그를 출력하기 위한 클래스를 만든다.

package hello.core.web;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import java.util.UUID;

@Component
@Scope(value = "request")
public class MyLog {
    private String uuid;
    private String requestURL;
    
    public void setRequestURL(String requestURL){
        this.requestURL = requestURL;
    }
    
    public void log(String message){
        System.out.println("UUID: " + uuid + ", requestURL: " + requestURL + ", message: " + message);
    }
    
    @PostConstruct
    public void init(){
        uuid = UUID.randomUUID().toString();
        System.out.println("UUID: " + uuid + ", request scope bean create: " + this);
    }
    
    @PreDestroy
    public void close(){
        System.out.println("UUID: " + uuid + ", request scope bean close: " + this);
    }
}

Scope를 request로 지정해서, HTTP 요청 당 하나씩 생성이 되고, 요청이 끝나는 시점에 소멸이 된다.

 

이제 Controller를 만든다.

package hello.core.web;

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequiredArgsConstructor
public class LogController {

    private final LogService logService;
    private final ObjectProvider<MyLog> myLogObjectProvider;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request){
        String requestURL = request.getRequestURL().toString();
        MyLog myLog = myLogObjectProvider.getObject();
        myLog.setRequestURL(requestURL);

        myLog.log("controller");
        logService.logic("testId");

        return "OK";
    }
}

request를 받아야 URL을 알 수 있기에 이 단계에서 URL을 넣어준다.

 

package hello.core.web;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogService {

    private final ObjectProvider<MyLog> myLogObjectProvider;

    public void logic(String id){
        MyLog myLog = myLogObjectProvider.getObject();
        myLog.log("service id = " + id);
    }
}

여기에서 ObjectProvider로 MyLog를 두개 생성하지만, 같은 HTTP 요청이기 때문에 동일한 빈이 들어간다.

ObjectProvider 덕분에 Object.getObject() 호출 전까지, request scope빈의 생성을 지연할 수 있다.

 

하지만 여기에서 더 나아가

 

스코프와 프록시

프록시 방식을 사용한다.

프록시 방식은

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLog{
}

로 proxyMode를 추가하는게 핵심이다.

적용 대상이 class면 TARGET_CLASS, 적용 대상이 인터페이스면 INTERFACES를 선택한다.

이렇게 하면 MyLog의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 주입해 둘 수 있다.

 

 

이렇게 myLog를 출력해보면, 이 객체를 상속받은 다른 객체가 출력되는 것을 볼 수 있다.

'Spring > 스프링' 카테고리의 다른 글

스프링 14일차  (0) 2023.03.26
스프링 13일차  (0) 2023.03.25
스프링 11일차  (0) 2023.02.12
스프링 10일차  (0) 2023.02.11
스프링 9일차  (0) 2023.02.10
728x90

인프런 김영한 님의 강의를 참고했습니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

빈의 생명주기

객체의 초기화 작업과 종료 작업이 어떻게 진행되는지 알아둘 필요가 있다.

예제 코드로

package hello.core.lifeCycle;

public class LifeCycleClient {

    private String print;

    public LifeCycleClient(){
        System.out.println("생성자 호출, print = " + print);
        beanStart();
        call("생성자!");
    }

    public void setPrint(String print){
        this.print = print;
    }

    //빈 시작시 호출
    public void beanStart(){
        System.out.println("beanStart: " + print);
    }

    public void call(String message){
        System.out.println("call: " + print + " message = " + message);
    }

    //빈 종료시 호출
    public void beanClose(){
        System.out.println("close: " + print);
    }
}

이런 class를 만들어보자.

우리는 이 class로 빈의 생명주기에 대해 알아볼 것이다.

package hello.core.lifecycle;

import hello.core.lifeCycle.LifeCycleClient;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class BeanLifeCycleTest {

    @Test
    public void lifeCycleTest(){
        ConfigurableApplicationContext applicationContext = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        LifeCycleClient client = applicationContext.getBean(LifeCycleClient.class);
        applicationContext.close();
    }

    @Configuration
    static class LifeCycleConfig{

        @Bean//(initMethod = "init", destroyMethod = "close")
        public LifeCycleClient lifeCycleClient(){
            LifeCycleClient networkClient = new LifeCycleClient();
            networkClient.setPrint("hello!");
            return networkClient;
        }
    }
}

그리고 테스트코드로 해당 빈을 열고 닫아보자.

실행해 보면

이렇게 print에 값이 들어가 있지 않은 것을 볼 수 있다.

생성자를 통해 생성하는 단계에는 print에 대한 값이 들어가있지 않고, 모두 생성이 된 후에 setter를 통해 값이 들어가기 때문이다.

스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 준비가 된다.

그렇기에 스프링은 의존관계 주입이 완료되면 스프링 빈에게 초기화 시점을 알려주는 기능을 제공한다. 당연히 스프링 컨테이너가 종료되기 직전에 소멸을 알려주는 기능도 제공한다.

 

스프링 빈의 LifeCycle

스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸 전 콜백 -> 스프링 종료

의 주기를 가진다.

 

물론 생성자에서 로직들을 추가하여 초기화를 할 수도 있지만, 유지보수를 위해 객체를 생성하는 부분과 초기화하는 부분을 분리는 것이 좋다.

 

생명주기 콜백

스프링은 3가지 방법으로 빈 생명주기 콜백을 지원한다.

InitializingBean, DisposableBean 인터페이스

설정 정보에 초기화 메서드, 종료 메서드를 지정

@PostConstruct, @PreDestroy annotation

 

  • InitializingBean, DisposableBean 인터페이스

InitializingBean, DisposableBean 인터페이스를 implements 하고 해당 인터페이스들에 있는 메서드들을 afterPropertiesSet(), destroy() 메서드들을 구현하는것이다.

package hello.core.lifeCycle;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class LifeCycleClient implements InitializingBean, DisposableBean {
    private String print;

    public LifeCycleClient() {
        System.out.println("생성자 호출, url = " + print);
    }

    public void setPrint(String print) {
        this.print = print;
    }

    //서비스 시작시 호출
    public void beanStart() {
        System.out.println("LifeCycleClient.beanStart, print = " + print);
    }

    public void call(String message) {
        System.out.println("call: " + print + " message = " + message);
    }

    //서비스 종료시 호출
    public void beanClose() {
        System.out.println("LifeCycleClient.disConnect, print = " + print);
    }


    public void afterPropertiesSet() throws Exception{
        System.out.println("NetworkClient.init");
        beanStart();
        call("초기화 연결 메시지");
    }

    public void destroy() throws Exception{
        System.out.println("NetworkClient.close");
        beanClose();
    }
}

이렇게 해당 메서드들을 구현하고 테스트를 진행해보자.

이렇게 생성자가 먼저 호출이 되고, 그 후에 초기화 콜백 메서드가 실행이 된다.

빈의 로직들이 실행이 되고, 소멸 전 콜백 실행 해 소멸이 되는 것을 볼 수 있다.

이 방법은 메서드의 이름을 변경할 수 없고 내가 코드를 고칠 수 없는 외부 라이브러리에는 적용이 불가능하여 스프링 초기에 사용하던 방법이고 최근에는 사용하지 않는다고 한다.

 

  • 설정 정보에 초기화 메서드, 종료 메서드를 지정

설정 정보에 @Bean(initMethod = "init", destroyMethod = "close")를 입력해 메서드들을 지정할 수 있다.

바로 해보도록 하자, 우선 LifeCycleClient를 좀 수정하고

package hello.core.lifeCycle;

public class LifeCycleClient{
    private String print;

    public LifeCycleClient() {
        System.out.println("생성자 호출, url = " + print);
    }

    public void setPrint(String print) {
        this.print = print;
    }

    //서비스 시작시 호출
    public void beanStart() {
        System.out.println("LifeCycleClient.beanStart, print = " + print);
    }

    public void call(String message) {
        System.out.println("call: " + print + " message = " + message);
    }

    //서비스 종료시 호출
    public void beanClose() {
        System.out.println("LifeCycleClient.disConnect, print = " + print);
    }

    public void init(){
        System.out.println("LifeCycleClient.init");
        beanStart();
        call("init!");
    }
    
    public void close(){
        System.out.println("LifeCycleClient.close");
        beanClose();
    }
}

@Bean에 initMethod와 DestroyMethod를 추가한다.

package hello.core.lifeCycle;

import hello.core.lifeCycle.LifeCycleClient;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class BeanLifeCycleTest {

    @Test
    public void lifeCycleTest(){
        ConfigurableApplicationContext applicationContext = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        LifeCycleClient client = applicationContext.getBean(LifeCycleClient.class);
        applicationContext.close();
    }

    @Configuration
    static class LifeCycleConfig{

        @Bean(initMethod = "init", destroyMethod = "close")
        public LifeCycleClient lifeCycleClient(){
            LifeCycleClient networkClient = new LifeCycleClient();
            networkClient.setPrint("hello!");
            return networkClient;
        }
    }
}

그러면

이렇게 생명주기에 맞게 실행이 되는 것을 볼 수 있다.

@DestroyMethod에 추록기능이 있어 close, shutdown을 찾아가지만 그래도 지정해주도록 하자.

 

  • @PostConstruct, @PreDestroy annotation

가장 많이 사용하는 방법이라고 한다, 하지만 외부 라이브러리에는 적용하지 못하기 때문에 외부 라이브러리에 사용하려면 위의 방법을 사용해야 한다고 한다.

예상했던대로

package hello.core.lifeCycle;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

public class LifeCycleClient{
    private String print;

    public LifeCycleClient() {
        System.out.println("생성자 호출, url = " + print);
    }

    public void setPrint(String print) {
        this.print = print;
    }

    //서비스 시작시 호출
    public void beanStart() {
        System.out.println("LifeCycleClient.beanStart, print = " + print);
    }

    public void call(String message) {
        System.out.println("call: " + print + " message = " + message);
    }

    //서비스 종료시 호출
    public void beanClose() {
        System.out.println("LifeCycleClient.disConnect, print = " + print);
    }

    @PostConstruct
    public void init(){
        System.out.println("LifeCycleClient.init");
        beanStart();
        call("init!");
    }

    @PreDestroy
    public void close(){
        System.out.println("LifeCycleClient.close");
        beanClose();
    }
}

그냥 이렇게 @PostConstruct, @PreDestroy annotation을 달아주는 방법이다.

 

'Spring > 스프링' 카테고리의 다른 글

스프링 13일차  (0) 2023.03.25
스프링 12일차  (0) 2023.02.15
스프링 10일차  (0) 2023.02.11
스프링 9일차  (0) 2023.02.10
스프링 8일차  (0) 2023.02.07

+ Recent posts