spring xml을 java config로

요즘에는 spring boot가 대세라 xml로 설정할 일이 없다.
그래서 아직 xml을 쓰는 사람을 위해 java config 설정 법을 간략하게 해보겠다.

일단 첫 번째 부터 한번 보자

public class BeanClass1 {
  public String getName(){
    return "hi youngjin";
  }
}

우리한테는 이런 클래스가 있다고 가정하자!
xml로 빈을 등록하기 위해 우리는 이렇게 했다.

<bean id="beanClass1" class="me.wonwoo.bean.BeanClass1"></bean>

위와 같이 메타정보를 xml로 설정 할때 이다. 클래스를 빈으로 설정한다.

잘되나 테스트를 해보자

@Test
public void springXmlConfig(){
  ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring/spring-servlet.xml");
  BeanClass1 beanClass1 = context.getBean(BeanClass1.class);
  System.out.println(beanClass1.getName());
}

깔끔하게 잘된다.

이번에는 java config로 바꾸어 보자
일단 SpringConfig라는 설정 클래스를 만들자. 그리고 다음과 같이 넣어보자

@Configuration
public class SpringConfig {

  @Bean
  public BeanClass1 beanClass1(){
    return new BeanClass1();
  }
}

그럼 이게 빈으로 등록 된거다. 간편하다. xml과 똑같은 일은 한다. 테스트를 해보자.

@Test
public void springAnnotationConfig(){
  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
  BeanClass1 beanClass1 = context.getBean(BeanClass1.class);
  System.out.println(beanClass1.getName());
}

참고로 spring의 메타 정보는 xml이 아니다. java config가 등장하기 전까지는 xml로만 해와서 오해를 부를 수도 있다.
spring의 메타정보는 추상화된 클래스다. 어떤식으로 파싱만 한다면 json으로 해도 무관하다.

생성자를 의존하는 빈을 등록을 해보자?(말이 이상한데?)
BeanClass2는 BeanClass1을 의존하고 있다.
아래와 같이 빈으로 생성자를 만들자.

public class BeanClass2 {

  private final BeanClass1 beanClass1;
  public BeanClass2(BeanClass1 beanClass1){
    this.beanClass1 = beanClass1;
  }
  public String getName(){
    return beanClass1.getName() + "!!!!";
  }
}

우리는 다음과 같이 xml을 설정 할 수 있다.

<bean id="beanClass1" class="me.wonwoo.bean.BeanClass1"></bean>

<bean id="beanClass2" class="me.wonwoo.bean.BeanClass2">
    <constructor-arg ref="beanClass1"></constructor-arg>
</bean>

beanClass1을 생성자로 참조하고 있다. 테스트를 해보자

@Test
public void springXmlConfig1(){
  ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring/spring-servlet.xml");
  BeanClass2 beanClass2 = context.getBean(BeanClass2.class);
  System.out.println(beanClass2.getName());
}

잘 된다.
다음으로 java config를 해보자

@Configuration
public class SpringConfig {

  @Bean
  public BeanClass1 beanClass1(){
    return new BeanClass1();
  }

  @Bean
  public BeanClass2 beanClass2(BeanClass1 beanClass1){
    return new BeanClass2(beanClass1);
  }
}

우리는 위와 같이 설정 가능하다.
BeanClass2의 파라미터로 beanClass1을 의존할 수 있다.

  @Bean
  public BeanClass2 beanClass2(){
    return new BeanClass2(beanClass1());
  }

설정 파일이 같다면 위와 같이 할 수도 있다.

다음으로는 setter에 넣어 보자

public class BeanClass3 {

  private BeanClass1 beanClass1;

  public void setBeanClass1(BeanClass1 beanClass1){
    this.beanClass1 = beanClass1;
  }

  public String getName(){
    return beanClass1.getName() + ". my name is wonwoo!";
  }
}

위와 같은 클래스가 있다고 가정하자

<bean id="beanClass1" class="me.wonwoo.bean.BeanClass1"></bean>

<bean id="beanClass3" class="me.wonwoo.bean.BeanClass3">
    <property name="beanClass1" ref="beanClass1"></property>
</bean>

우리는 위와 같이 설정 가능하다.
테스트를 해보자

@Test
public void springXmlConfig2(){
  ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring/spring-servlet.xml");
  BeanClass3 beanClass3 = context.getBean(BeanClass3.class);
  System.out.println(beanClass3.getName());
}

다음으로 java config를 보자.
생성자도 해봤으니 딱히 어려운 부분은 없다.

@Configuration
public class SpringConfig {

  @Bean
  public BeanClass1 beanClass1(){
    return new BeanClass1();
  }

  @Bean
  public BeanClass3 beanClass3(BeanClass1 beanClass1){
    BeanClass3 beanClass3 = new BeanClass3();
    beanClass3.setBeanClass1(beanClass1);
    return beanClass3;
  }
}

이렇게 동일한 방식으로 하면 된다.
이렇게 xml보다 java config가 더 깔끔한듯 싶다.
boot를 이용하려면 이정도는 알아야 되지 않나 싶다.
소스는 github에 올라가 있다.

private 생성자를 사용하자

제목이 조금 이상하긴한데.. 모든 클래스에 해당되는 말은 당연히 아니다
우리가 흔히 쓰는 유틸 클래스 상태가 없는 클래스들을 말하는거다.
해당 클래스들은 모두 static 메소드를 이루어져 있다.
객체를 인스턴스할 필요가 없는 클래스들 이다.
흔히 유틸 클래스를 만들 때 인스턴스화를 막기 위해 클래스의 abstract 선언하곤 한다.
abstract 선언해도 인스턴스를 생성 할 수 있다.
예를 들어 보자


@Test public void test() { ReflectionClass reflectionClass = new ReflectionClass() { }; reflectionClass.printName("wonwoo"); } abstract class ReflectionClass { public ReflectionClass() { System.out.println("init"); } public static void printName(String name){ System.out.println(name); } }

우리는 printName를 인스턴스화 해서 호출하였다.
물론 이 방법을 써도 컬리브레이스가 자동으로 생성되기 때문에 (툴에서) 알 수는 있다.
행여나 실수를 방지 하기 위해 가능하면 생성자를 private로 하자.
그러면 컴파일시 에러가 발생하므로 더 명확하다.
하지만 private를 생성자로 선언해도 접근가능하다.
코드를 보자.

Class classes = ReflectionClass.class;
Constructor constructor = classes.getDeclaredConstructor();
constructor.setAccessible(true);
Object o = constructor.newInstance();
System.out.println(o);

class ReflectionClass {
    private ReflectionClass() {
        System.out.println("init");
    }
}

우리는 다음과 같이 init을 출력하는걸 볼 수 있다.
그래서 우리는 다음과 같이 할 수 있다.

class ReflectionClass {
    //no instance
    private ReflectionClass() {
        throw new AssertionError();
    }
}

물론 컴파일시에는 알 수는 없지만 런타임시에라도 에러를 발생 시키자.
그리고 따로 주석까지 남기는건 센스

어제 놀다가 늦게 들어갔는데 책이 와있었다.
피곤한 나머지 몇장 못보고 잤지만 대충 이런 내용인듯 싶다.
혹시나 다르다면 나중에 고쳐야지

spring jpa의 patch때 사용하는 유틸

회사에서 잉여짓만하는구만ㅠㅠ
스프링 jpa를 쓰다보면 뭐 form으로 전달하는 것은 상관이 없는데 API 를만들다보면 업데이트시 null로 오면
null을 업데이트 한다.
그래서 옛날객체의 새로운 객체를 넣어 주어 update 하는 방식으로 해왔다.
예를 들어 이런 방식이다.

if(account.getName() == null){
  oldAccount.setName(account.getName())
}

필드가 많다보면 이것도 일이다. notnull은 상관없는데 null을 허용하는 것들이 문제다.
그래서 유틸로 만들었다.

public static <T> void oldInstanceBynewInstance(T oldInstance, T newInstance) {
    Class<?> newInstanceClass = newInstance.getClass();
    Class<?> oldInstanceClass = oldInstance.getClass();

    if(!newInstanceClass.isAssignableFrom(oldInstanceClass)){
        return;
    }

    for (Field newField : newInstanceClass.getDeclaredFields()) {
        newField.setAccessible(true);
        Object obj;
        try {
            obj = newField.get(newInstance);
        } catch (IllegalAccessException e) {
            throw new ReflecationException("reflecation Exception get field");
        }
        Id id = newField.getAnnotation(Id.class);
        PatchIgnore patchIgnore = newField.getAnnotation(PatchIgnore.class);
        if (id == null) {
            if (obj != null && patchIgnore == null) {
                Field oldField;
                try {
                    oldField = oldInstanceClass.getDeclaredField(newField.getName());
                    oldField.setAccessible(true);
                    oldField.set(oldInstance, obj);
                } catch (NoSuchFieldException e) {
                    throw new ReflecationException("no such field");
                } catch (IllegalAccessException e) {
                    throw new ReflecationException("reflecation Exception set field");
                }
            }
        }
    }
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PatchIgnore {
}

새로운객체의 필드를 오래된 객체에 넣는것 뿐이다.
테스트를 해보자

public class ReflectionUtilsTest {

    Product oldProduct = new Product();

    @Before
    public void before() {
        oldProduct.setId(1L);
        oldProduct.setName("wonwoo");
        oldProduct.setEmail("email@test.com");
        oldProduct.setPassword("password");
    }

    @Test
    public void reflectionUtilTest() {
        Product product = new Product();
        product.setId(20L);
        product.setName("ooooo");
        product.setEmail("wonwoo@test.com");
        ReflectionUtils.oldInstanceBynewInstance(oldProduct, product);
        assertEquals(oldProduct.getId().longValue(), 1L);
        assertEquals(oldProduct.getName(), "ooooo");
        assertEquals(oldProduct.getEmail(), "wonwoo@test.com");
    }

    @Test
    public void reflectionUtilIgnoreTest() {
        Product product = new Product();
        product.setName("ooooo");
        product.setEmail("wonwoo@test.com");
        product.setPassword("repassword");
        ReflectionUtils.oldInstanceBynewInstance(oldProduct, product);
        assertEquals(oldProduct.getName(), "ooooo");
        assertEquals(oldProduct.getEmail(), "wonwoo@test.com");
        assertEquals(oldProduct.getPassword(), "password");
    }
}

@Data
class Product {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private String email;

    @PatchIgnore
    private String password;
}

Id 어노테이션과 PatchIgnore은 무시한다.
old객체의 name 에 wonwoo라는 String이 들어가 있다. 그리고 나서 새로운 객체에 ooooo 을 넣었다.
그러면 old객체에 ooooo이 들어간걸 확인 할 수 있다.
또한 PatchIgnore 어노테이션은 무시한다. 기존에 password 라고 넣고 새로운 객체에는 repassword라 넣었다.
하지만 무시당하고 password가 계속 담겨 있다.
실직적으로 사용은 이렇게 하면 된다.

public Account update(final Long id, final Account account) {
    Account oldAccount = findOne(id);
    if (oldAccount == null) {
        throw new IDNotFoundException("id " + id + " not found");
    }
    ReflectionUtils.oldInstanceBynewInstance(oldAccount, account);
    return oldAccount;
}

jpa 의 변경감지로 인해 굳이 save를 할 필요 없다
필자가 생각한방법이다.
물론 더 좋은 방법이 있을 수도 있지만 지금 현재 나는 모른다.ㅜㅜㅜ