2월 25, 2024

[JPA] @CreatedDate, @CreatedBy, @LastModifiedDate, @LastModifiedBy, auditorAware, @EnableJpaAuditing 통해서 생성일시, 변경일시 관리하기

1. Intro

데이터를 관리할 때 처음 저장된 일시, 유저, 마지막으로 저장된 일시, 유저 데이터를 관리하는 것은 중요하다. 오늘은 JPA에서 annotation을 사용해서 이들을 관리하는 방법을 알아보도록 하겠다. 

2. Annotation

1) @CreatedDate

데이터가 "생성된 시간 정보"를 관리하는 annotation이다. 참고로 main method가 실행되는 class에 @EnableJpaAuditing annotation을 적용하여야 이 일시에 null이 아닌, 실제 생성된 시간 정보가 들어간다. 

3. @CreatedBy

처음 데이터를 생성한 user 정보를 넣어준다. @CreatedDate와는 달리 따로 Spring에서 알 수 있는 방법이 없기 때문에 따로 auditorAware를 사용하여 구현해줄 수 있다.

package com.example.projectboard.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

import java.util.Optional;

@EnableJpaAuditing
@Configuration
public class JpaConfig {

@Bean
public AuditorAware<String> auditorAware(){
return () -> Optional.of("user1");
}
}

3) @LastModifiedDate

2번 @CreatedDate와 비슷하게 일시를 나타내주는 데이터인데, @LastModifiedDate를 통해 가장 마지막으로 변경된 일시를 나타내준다. 

change된 데이터의 이력을 관리하는 목적으로 사용된다.

4) @LastModifiedBy

3번 @CreatedBy와 유사하게 데이터를 변경한 사용자를 관리하는 목적으로 사용된다. @CreatedBy가 가장 처음에 데이터를 생성한 유저를 나타낸다면 @LastModifiedBy는 가장 마지막으로 데이터를 변경한 유저를 나타낸다. 

3. 예시코드 - 생성일시, 변경일시 등을 관리하는 게시글 Entity 만들기

위 네 개의 데이터를 관리할 수 있는 것이 대표적으로 게시글이라고 할 수 있다. Article이라는 이름으로 Entity를 만들면 아래와 같이 만들 수 있다. 전체 코드를 아래와 같이 살펴보자. 

package com.example.projectboard.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;

import java.time.LocalDateTime;
import java.util.Objects;

@Getter
@ToString
@Table(indexes = {
@Index(columnList = "title"),
@Index(columnList = "createdAt"),
@Index(columnList = "createdBy")
})
@Entity
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Setter
private String title;
@Setter @Column(nullable = false) private String content;

@CreatedDate @Column(nullable = false) private LocalDateTime createdAt;
@CreatedBy @Column(nullable = false, length = 200) private String createdBy;
@LastModifiedDate @Column(nullable = false) private LocalDateTime modifiedAt;
@LastModifiedBy @Column(nullable = false, length = 200) private String modifiedBy;

protected Article(){

}

private Article(String title, String content) {
this.title = title;
this.content = content;
}

private Article of(String title, String content) {
return new Article(title, content);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Article article)) return false;
return id != null && id.equals(article.id);
}

@Override
public int hashCode() {
return Objects.hash(id
);
}
}

참고로 id는 @GeneratedValue를 사용하여 생성되도록 하였다. 

나머지는 @Column annotation을 사용하여 nullable 여부와 length를 표시해주었다. 

2월 05, 2024

@BeforeEach 사용하여 Reflections 초기화해주기, BeanFactory Test Code Java로 구현해보기

1. BeanFactory란? 

Spring에서 핵심이 되는 개념으로, Bean을 생성하고 의존관계를 설정하는 기능을 담당하고 있는 컨테이너를 의미한다. 


import java.util.Set;

public class BeanFactory {
private final Set<Class<?>> preClass;
public BeanFactory(Set<Class<?>> preClass) {
this.preClass = preClass;
}
}


위와 같이 BeanFactory의 코드를 작성해줄 수 있다. 



그리고 BeanFactory의 테스트코드를 작성하기 전 기초가 되는 코드를 아래와 같이 작성할 수 있다. 


2. BeanFactory 테스트 코드 전 초기화 코드


import static org.junit.jupiter.api.Assertions.*;

import org.example.annotation.Controller;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.reflections.Reflections;

import java.lang.annotation.Annotation;
import java.util.HashSet;
import java.util.Set;

class BeanFactoryTest {
    private BeanFactory beanFactory;
private Reflections reflections;

@BeforeEach
void setUp(){
reflections = new Reflections("org.example");
Set<Class<?>> preClass= getClassAnnotatedWith(Controller.class);
beanFactory = new BeanFactory(preClass);
}

private Set<Class<?>> getClassAnnotatedWith(Class<? extends Annotation>... annotations) {
Set<Class<?>> beans = new HashSet<>();
for (Class<? extends Annotation> annotation: annotations){
beans.addAll(reflections.getTypesAnnotatedWith(annotation));
}
return beans;
}
}


여기서 Test Code의 

@BeforeEach annotation은 각 test code를 수행하기 전 위 코드를 먼저 수행한다는 뜻이다. 

각 Test Code가 돌기 전 

org.example 밑에 있는 Class를 돌면서 Reflections를 초기화해준다. 

그리고 Controller Annotation을 만들어주었다고 치면 


Controller Annotation이 붙은 class들의 set을 찾아 Set<Class<?>>로 return 받을 수 있다. 


이를 위하여 별도의 getClassAnnotatedWith라는 메소드를 만들었고 이는 parameter로 들어온 Annotation들이 붙어있는 class들을 찾아 Class들의 Set으로 반환하는 메소드이다. 


그러면 이러한 set들로 bean을 생성해주는 것까지 BeanFactoryTest class의 초기화단계라고 해줄 수 있다. 


여기서 Parameter로 여러 Annotation을 받을 수 있기 때문에 getClassAnnotatedWith 메소드에서는 가변인자로 Annotation Class를 받아주었다. 가변인자 관련해서는 다음 포스팅에서 더 자세히 다루어보도록 하겠다. 



8월 30, 2023

[Spring] JDBC를 통한 데이터 접근, RowMapper 인터페이스, NamedParameterJdbcTemplate

Java Spring에서 관계형 데이터베이스를 통해 쿼리를 실행하고 데이터를 변경하고 싶다면 어떤 식으로 실행해야 할까? 이것을 구현하기 위해 알아야 하는 개념으로 RowMapper, NamedParameterJdbcTemplate 등이 있다. 

 

1. RowMapper

RowMapper란 JDBC 기본 인터페이스 ResultSet에서 원하는 객체로 타입을 변경해주는 역할을 한다. 

import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;

위와 같이 import를 해주면 RowMapper와 ResultSet을 사용할 수 있다. 

 

예를 들어, 

public class Person {
 private String firstName;
 private String lastName;
}

과 같은 class가 존재한다고 가정해보자. 

 

그리고

@Component
public class PersonRowMapper implements RowMapper<Person> {
	public Person mapRow(ResultSet resultSet, int rowNum) throws SQLException {
     Person person = new Person();
     person.setFirstName (resultSet.getString(1));
     person.setLastName (resultSet.getString(2));
     return person;
    }
}

위와 같이 PersonRowMapper class가 RowMapper 인터페이스를 구현했다고 가정하면, 아래와 같은 코드를 쓰는 것이 가능해진다. 

 

@Service
public class PersonServiceImpl implements PersonService {
  private static final String SEARCH_ALL =
  	"select first_name, last_name";
  
  @Inject private NamedParameterJdbcOperations jdbcTemplate;
  @Inject private PersonRowMapper personRowMapper;
  
  
public List<Person> getPeople() {
  return jdbcTemplate.query{
  	SEARCH_ALL, new HashMap<String, Object>(), personRowMapper;
  }
}
   
}

static final로 실행하고자 하는 select sql을 하나 만들었고 NamedParameterJdcbOperations를 통해 해당 sql를 실행한다. (jdbcTemplate.query 부분) 

 

해당 select sql을 실행하게 되면 select로 여러 값들이 반환될 텐데, 그 값들을 RowMapper를 사용하여 원하는 객체 형태인 Person 형태들로 변환이 되게 되는 것이다. 결국 getPeople 메서드에서는 personRowMapper를 통해 sql 실행한 값들을 원하는 Person 객체들로 받아 List<Person>으로 반환할 수 있게 되는 것이다. 


2. NamedParameterJdbcTemplate

NamedParameterJdbcTemplate 에서는 변수 매핑을 ? 로 하던 기존 JDBC와는 달리 이름을 통해서 변수 매핑을 가능토록 만들어준다. 변수 이름 앞에  " : " 이것을 써서 가능하게 만들어준다. 

 

위 NamedParameterJdbcTemplate를 사용하여 Person 객체 하나를 더 만드는 메서드 하나와 기존 존재하는 Person 객체를 update하는 메서드 하나를 만들어보겠다. 

 

class는 위에서 사용하던 PersonServiceImpl과 동일한 class이고 위에서 사용한 코드에 추가하여 코드를 완성해보도록 하겠다. 

 

@Service
public class PersonServiceImpl implements PersonService {
  private static final String SEARCH_ALL =
  	"select first_name, last_name";
  
  //추가, createPerson 메서드에서 활용
  private static final String CREATE =
    "insert into person (first_name, last_name) " +
    "values (:firstName, :lastName)";
  //추가, updatePerson 메서드에서 활용
  private static final String UPDATE =
  	"update person set first_name = :firstName, " +
    				"where last_name = :lastName"; 
  @Inject private NamedParameterJdbcOperations jdbcTemplate;
  @Inject private PersonRowMapper personRowMapper;
  
  
public List<Person> getPeople() {
  return jdbcTemplate.query{
  	SEARCH_ALL, new HashMap<String, Object>(), personRowMapper;
  }
}
 // Create sql
 public void createPerson (Person person) {
  SqlParameterSource params= new MapSqlParameterSource()
  	.addValue("firstName", person.getFirstName())
    .addValue("lastName", person.getLastName());
  KeyHolder keyHolder= new GeneratedKeyHolder(); //자동생성되는 key 값을 위해
  jdbcTemplate.update(CREATE, params, keyHolder);
  
  //update
  public void updatePerson(Person person){
  	SqlParameterSource params= new MapSqlParameterSource()
  	.addValue("firstName", person.getFirstName())
    .addValue("lastName", person.getLastName());
    jdbcTemplate.update(UPDATE,params);
  }
 }
}

 

코드를 조금 더 자세히 살펴보면 createPerson 메서드에서는 데이터를 insert 하고 있는데, insert 명령문에서는 자동 생성되는 key 값을 내포하는 의미가 있으므로 GeneratedKeyHolder도 같이 생성하게 된다. 

 

여기서 살펴보아야 할 점은 NamedParameterJdbcTemplate를 사용하니 :콜론을 사용하여 자동으로 객체변환을 시켜주었다는 점이다. 

 

즉 데이터베이스 테이블 상 first_name, last_name (snake_case) 이었던 칼럼과 firstName, lastName (camelCase)로 자동매핑 되었다. 이와 같이 각각의 매개 변수 이름 앞에 콜론 기호를 접두사로 사용하여 자동매핑을 시켜주고 정확하게 의미를 구분하고 있다. 


8월 30, 2023

[Spring] @Autowired annotation 사용법

[Spring] p 네임스페이스, c 네임스페이스란? + ref 사용법

 

지난 포스팅에서는 p 네임스페이스와 ref를 사용하여 Bean에 DAO 등을 주입하는 방법을 알아보았다. 

오늘은 @Autowired 어느테이션을 사용하여 

p:[Dao name]-ref="daoName"

과 같은 특성을 더 이상 사용하지 않는 방법을 알아보도록 할 것이다. 


@Autowired를 사용할 수 있는 위치는 아래와 같이 세 가지가 있다.

  1. 생성자
  2. Setter
  3. 필드

각각의 방식에서 @Autowired를 사용할 수 있는 방법을 알아보도록 하자.


1. 생성자에 @Autowired를 사용하는 방법

 

@Service
public class AccountService {
 AccountDao accountDao;
 
 @Autowired
 public AccountService(AccountDao accountDao){
 	this.accountDao=accountDao;
 }
}

위와 같은 accountService class가 하나 있다고 가정해보자.

위의 @Autowired가 적용되기 위해서 AccountDao class는 어떤 형태를 띄어야 할까?

 

public class AccountDao {
 //
}

단순히 위와 같은 class로 짜여있으면 작동하지 않을 것이다. 

 

@Repository
public class AccountDao {
 //
}

위와 같이 @Repository annotation이 존재해야 작동하는데, 그 이유는 @Autowired가 의존 객체 type의 bean들을 찾아서 주입해주기 때문이다. @Repository annotation이 없으면 AccountDao를 bean으로 인식하지 못하여 의존성 주입에 실패하게 된다.


2. Setter에 @Autowired를 사용하는 방법

@Service
public class AccountService {

    AccountDao accountDao;
	
    @Autowired(required = false)
    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }
}
public class AccountDao {
 //
}

 

두 번째 방법은 위와 같이 setter method에 @Autowired annotation을 사용해주는 방법이다. 

여기서 

@Autowired(required = false)

위와 같이 required = false 옵션을 추가로 주게 되면 해당 bean이 없을 경우 주입받지 않을 수 있다는 뜻을 가진다. 만약 뒤에 required = false를 쓰지 않으면 기본값은 true 를 가진다. 즉 만약 requred = false가 추가로 들어가게 되면 해당 bean (여기서는 AccountDao) 이 호출조차 되지 않는다고 보면 된다. 


3. 필드에 @Autowired 를 명시하는 방법

@Service
public class AccountService {
	
    @Autowired
    AccountDao accountDao;
}

위와 같이 필드 자체에 @Autowired annotation을 사용하는 방법도 있다. 

 


여기서 만약 해당하는 형태의 Bean이 여러 개이면 어떻게 될까? 즉 만약 위와 같은 AccountService에서 Autowired로 연결된 AccountDao Bean이 여러 종류일 경우를 가정해보자.

 

public interface AccountDao {
	//AccountDao interface
}

@Repository
public class FirstAccountDao implements AccountDao {
  //
}

@Repository
public class SecondAccountDao implements AccountDao {
  // 
}

 

즉 위와 같이 AccountDao interface가 하나 존재하고 해당 인터페이스를 여러 Bean에서 객체로 구현을 할 경우이다. 여기서 FirstAccountDao, SecondAccountDao 모두 @Repository annotation이 있어, 둘 다 Bean으로 등록되어 있는 상황이다. 

 

이런 상황에서 AccountService class에서 아래와 같이

@Service
public class AccountService {
	
    @Autowired
    AccountDao accountDao;
}

AccountDao Bean을 @Autowired로 연결하면 어떤 AccountDao가 연결이 될까?

 

이 경우 스프링에서는 에러를 뱉는다.

왜냐하면 연결되는 Bean을 찾는데 두 개 이상이 나오게 되면 어떤 의존성을 주입해야 하는지 스프링에서는 알 길이 없기 때문이다. 


8월 30, 2023

XML 파일을 통해 Spring Bean을 정의하고 구성하기 (property, ref, ID, class)

지난 포스팅에서 Spring을 통한 의존성 주입과 제어역전의 개념을 알아보았다.


Spring 의존성 주입(DI), DI container, 제어의 역전 (IoC) 한번에 이해하기 

 

Spring을 이용하여 객체를 구성하고 의존성을 연결하는 여러 가지 방법들 중 가장 일반적인 방법은 XML, 그리고 선언적 방법이 있다. 

 

이번 포스팅에서는 Spring을 이용하여 XML 파일에서 Bean을 정의하고 구성하는 방법을 알아보도록 하자.

 

1. Bean ID, Class

<bean id="testBean" class="com.springPractice.TestBean" />

Bean의 ID는 Spring container에서 식별할 수 있는 이름, class는 해당 Bean의 Full 경로를 의미한다. 위 예제에서는 ID가 testBean, 그리고 해당 Bean의 full path 즉 com.springPractice.TestBean이 해당 Bean의 전체경로이다.

Spring에서는 ID 속성을 사용하여 Bean 사이의 종속성을 지원할 수 있다.


 

2. Bean Property

Bean xml에서

<property name="" value =""/>

이런식으로 property 속성을 통해 setter method를 타고 인스턴스를 정의할 수 있다.

 

이렇게만 보면 이해하기가 어렵기 떄문에 예제를 통해 설명을 해보도록 하겠다.

 

public class Person {
	//Person Class의 instance 
    
    private String name;
    private int age;
    
    public void setName(String name){
    	this.name=name;
    }
    public void setAge(int age){
    	this.age=age;
    }
}

 

위와 같이 name과 age를 인스턴스로 가지고 있는 Person class에 setter method로 setName과 setAge가 존재한다고 가정해보자. 그러면 우리는 Spring의 xml 파일에 property name, value 속성을 사용해서 setter method를 사용하는 것과 동일한 효과를 얻을 수 있는 것이다. 

 

xml 파일에서 Tommy라는 이름을 가진 25살 사람을 만들어준다고 해보자. 원래였다면 setter 메소드를 사용하여 주입해주었겠지만 우리는 아래와 같이 작성할 수 있다.

 

<bean id="person" class="com.SpringPractice.person">
	<property name="name">
    	<value>Tommy</value>
    </property>
    <property name="age" value="25"/>
</bean>

 

즉 property name에는 instance의 이름, value에는 실제 set 하려는 값을 적어주면 된다.


 

3. ref 

Ref 특성은 Spring을 이용하여 의존성 주입을 할 때 사용된다. value 특성과 다른점은 무엇일까. vlaue 특성은 값 객체 속성이나 기본 형식의 데이터를 주입하기 위하여 사용되는 반면, ref의 경우 참조형으로 id 혹은 name을 가지는 bean을 생성자의 인자로 넘겨주겠다는 의미로 이해하면 된다. 

 

즉 예를 들어 Service.java라는 파일 안에 testDao instance가 있다면,

<bean id="Service">
  <property name ="testDao" ref="firstDao"/>
</bean>

 

위와 같이 testDao instance에 firstDao를 서비스로 의존성 주입한 것이라고 생각하면 된다. 


이런식으로 Spring에서 xml 파일을 이용하여 의존성을 주입하는 방법, 그리고 그 속성들에 대하여 알아보았다.


8월 30, 2023

Spring 의존성 주입(DI), DI container, 제어의 역전 (IoC) 한번에 이해하기

Spring 프레임워크를 이해하는 것은 스프링프레임워크의 핵심이 의존성 주입 컨테이너라는 사실을 아는 것과, 

다른 모듈들이 작동하기 위하여 필요한 기능을 스프링의 의존성 주입 컨테이너가 제공한다는 사실을 아는 것에서 출발한다.

 

의존성 주입은 어려운 개념은 아니지만 개념을 완전히 이해해야 향후 스프링을 이해하는 것에도 문제가 없다.

 

1. 의존성 주입 (Dependency Injection)

스프링의 가장 기본 뼈대가 되는 의존성 주입이라는 개념에 대해서 알아보자.

 

먼저 "의존성"이란 한 객체가 다른 객체를 사용할 떄 의존성이 있다고 말한다. 

장난감을 주로 팔고 있는 상점을 예시로 생각해보자.

 

public class Store {
	private Toy toy;
}

 

그러면 위와 같이 Store이라는 class 안에 Toy라는 instance를 가지고 있는 구조를 띌 것이다.

 

이럴 떄 우리는 Store가 Toy에 의존성이 있다고 말한다. 즉 Store라는 class가 Toy class를 사용할 때 의존성을 가지는 관계라고 말을 하는 것이다. 

 


 

의존성을 가지고 있을 경우, 우리가 코드를 쓰는 가장 간단한 방법은 아래와 같다.

 

public class Store {
  private Toy toy;
  
  public Store() {
   this.toy= new Toy();
  }
}

 

하지만 위 클래스의 문제는 무엇일까?

 

바로 Store class와 Toy class가 강하게 결합되어 있고 의존성이 높다는 점이다. 만약 Store에서 판매하는 객체의 종류가 달라진다면, Store class 밖에서 무엇인가를 변경하는 것이 아닌, Store class의 생성자 자체를 변경해야 하는 상황이 오게 된다. 

 

이러한 의존성을 제거하기 위해서 Spring에서 자주 쓰는 기법으로

의존성 주입 (Dependency Injection : DI) 라는 개념을 소개하도록 하겠다.

 

의존성 주입이란 객체를 직접 생성하는 것이 아니라, 외부에서 생성한 후 주입시켜 주는 방식으로, 의존성을 낮추어주는 방식을 말한다.

 

위 예시에서는 Store에서 판매하고 있는 상품의 종류가 바뀔 수도 있는 경우를 대비해 

Store안에서 바로 객체 Toy를 생성하는 것이 아니라 외부에서 생성한 후 이를 주입해주어야 한다. 

 

이를 위해서는 Toy의 상위 개념 Product Interface를 하나 생성할 것이다.

 

public interface Product {
 //Toy의 상위 interface
}

public class Toy implements Product {
//Product interface를 구현한 Toy class
}

 

이렇게 상위 Interface를 생성한 뒤, Store class에서 바로 Toy 객체를 생성하는 것이 아니라 

상위 인터페이스로 Product 객체를 생성하는 것이다.

 

public class Store{
 private Product product;
 
 public Store (Product product){
 //생성자를 Toy 객체가 아닌 Product 객체로 받음
 	this.product=product;
 }
}

 

Spring container가 관리하는 객체를 빈 (Bean)이라고 부르며, 이러한 bean들을 관리한다는 의미로 컨테이너를 Bean Factory라고 부른다. 이는 디자인 패턴 중 Factory Design 패턴과도 연관이 있다.

 

따라서 BeanFactory라는 이름의 class를 생성하여 어떻게 의존성이 주입되는 것인지 살펴보겠다. 

 

public class BeanFactory{
	public void create(){
    	Product toy=new Toy(); //Bean 객체 생성
        
        Store store=new Store(toy); //생성자를 통한 의존성 주입
    }
}

위와 같은 방법으로 어플리케이션 실행 시점에 필요한 Bean이 생성되었고 해당 객체를 주입하고자 하는 객체에 주입하는 역할로 BeanFactory, 즉 DI container가 사용되었다. 


2. 제어의 역전 (Inversion of Control)

그리고 제어의 역전(Inversion of Control) 우리가 흔히 부르는 IoC라고 불리는 개념도 의존성 주입이라는 개념으로부터 파생된다.

어떠한 객체를 사용할지에 대한 책임 (ex 위 예시에서 Toy 객체) 은 프레임워크에게 넘기고, 자신은 수동적으로 주입받는 객체를 사용하기 때문이다. 제어가 프레임워크에 넘어갔고, 제어가 역전되었다는 의미로 제어의 역전이라는 용어를 이해하면 어렵지 않게 이해할 수 있을 것이다. 


오늘 포스팅의 예시는 Spring 에서 의존성 주입을 받는 방법 중 한 가지 예시이다. 이외에도 방법이 있으며 오늘 포스팅을 통해서는 대강 흔히 말하는 의존성 주입, 제어의 역전이 어떠한 개념적인 의미를 가지는지를 이해하는 것이 중요하다.