2011년 10월 18일 화요일

private 접근자를 가진 객체의 필드에 접근하기.

가끔 오픈 소스나 라이브러리를 가지고 개발해야 하는 경우에 특정 객체의 필드에 접근해야 할 필요가 있다.
아래의 샘플 코드를 살펴 보자.

package test.java;

import java.util.HashMap;
import java.util.Map;

public class InsertableContainer {

  private Map map = new HashMap();

  public InsertableContainer() {
    map.put("firstName", "Younggyu");
    map.put("lastName", "Kim");
    map.put("age", "25");
  }
  
  public void insert(String key, String value) {
    if(map.containsKey(key)) {
      throw new IllegalArgumentException("key already exists: " + key);
    }
    map.put(key, value);
  }
  
  public void display() {
    System.out.println(map);
  }
}

위의 클래스는 프로퍼티를 추가하는 insert 메서드와 설정된 정보를 출력하는 display 메서드만 제공하는 간단한 클래스다.
그런데 insert 메서드에서는 이미 키가 존재하는 경우에 IllegalStateException을 발생시키기 때문에 기존에 설정된 프로퍼티값을 변경할 수가 없다.
InsertableContainer를 설계할 당시에는 평생 25살로 살거라 생각했었는데, 어느덧 35살이 되었기 때문에 age 속성을 변경해야할 필요가 생겼다.
public 접근자를 가진 메서드만 가지고는 age를 수정시킬 방법이 없다.
아래의 테스트 코드는 IllegalArgumentException을 발생시킨다.

  @Test(expected=IllegalArgumentException.class)
  public void testField() {
    InsertableContainer container = new InsertableContainer();
    container.display();
    
    container.insert("age", "35");
  }
이런 경우에 Reflection을 이용하여 InsertableContainer의 map 필드를 읽어 내서 수정하는 방법을 고려해 보자.
아래의 소스 코드를 살펴보자.
리플렉션을 이용하여 InsertableContainer의 private 접근자를 가진 'map' 필드를 읽어 내고 있다.
  @SuppressWarnings("unchecked")
  private Map getContainerMap(InsertableContainer container) {
    Map map = null;

    try {
      Field field = InsertableContainer.class.getDeclaredField("map");
      field.setAccessible(true);
      map = (Map)field.get(container);
    } catch(Exception ex) {
      ex.printStackTrace();
    }
    return map;
  }
위의 소스 코드에서 field.setAccessible(true); 소스 코드가 private 필드에 접근이 가능하게 한다.
accessible을 false 로 지정하거나, 아예 지정하지 않으면 private Field에 접근할 수 없기 때문에 아래와 같이 오류가 발생한다.

"java.lang.IllegalAccessException: Class test.java.FieldTest can not access a member of class test.java.InsertableContainer with modifiers "private"".

테스트 소스 코드를 아래와 같이 수정하면 이제 오류가 발생하지 않고 정상적으로 동작한다.
  @Test()
  public void testField2() {
    InsertableContainer container = new InsertableContainer();
    container.display();

    Map map = getContainerMap(container);
    map.put("age", "35");
    
    container.display();
  }

화면 출력은 아래와 같다.

{lastName=Kim, age=25, firstName=Younggyu}
{lastName=Kim, age=35, firstName=Younggyu}

2011년 10월 13일 목요일

How to use mybatis mapper


mybatis를 이용하면 XML 매핑 파일을 작성하는 대신 annotation 이용하여 개발이 가능하다.


SQL 코드를 클래스로부터 분리하여 별도의 리소스(sqlmap xml)파일로 관리할 수 있도록 하는 방식에서
인터페이스 안에 매핑정보를 넣어야 하는 게 올바른 방향인지는 궁금하다.

쿼리가 변경되면 인터페이스를 재 컴파일해서 올려야 할 듯.
아무튼 간단히 개발하는 곳에서는 적용해볼만 한듯해서 관련 소스를 올려둔다.

  • 스프링 설정 파일

    스프링 설정 파일에서는 dataSource, sqlSessionFactory 및 매퍼 인터페이스를 지정해야 한다.

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
      xsi:schemaLocation="
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-3.0.xsd">
    
      <context:component-scan base-package="com.archnal.samples.mybatis"/>
      
      <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close">
        <property name="driverClassName" value="${jdbc.driverClassName}" />
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />
      </bean>
      <context:property-placeholder location="spring/jdbc.properties" />
    
      <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
      </bean>
      <bean id="accountMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
        <property name="mapperInterface"
          value="com.archnal.samples.mybatis.mapper.AccountMapper" />
        <property name="sqlSessionFactory" ref="sqlSessionFactory" />
      </bean>
    
    
    </beans>
    
  • Mapper 인터페이스

    Mapper 인터페이스에서는 mybatis에서 제공하는 @Select, @Insert 등의 annotation을 이용하여 쿼리를 작성할 수 있다.

    package com.archnal.samples.mybatis.mapper;
    
    import org.apache.ibatis.annotations.Insert;
    import org.apache.ibatis.annotations.Param;
    import org.apache.ibatis.annotations.Select;
    
    import com.archnal.samples.mybatis.dto.Account;
    
    public interface AccountMapper {
      String INSERT = "INSERT INTO tbl_account (name, email) values (#{name}, #{email})";
    
      
      @Select("SELECT * FROM tbl_account WHERE email = #{email}")
      Account getByEmail(@Param("email") String email);
      
      @Insert(INSERT)
      void insert(Account account);
    }
    
    
  • test 클래스

    아래의 소스 코드에서 처럼 실행하면 실제로 데이터베이스에서 값을 읽어 오거나 추가시킬 수 있다.

    package com.archnal.samples.mybatis.mapper;
    
    import javax.annotation.Resource;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.test.annotation.Rollback;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    import org.springframework.test.context.transaction.TransactionConfiguration;
    import org.springframework.transaction.annotation.Transactional;
    
    import com.archnal.samples.mybatis.dto.Account;
    
    @ContextConfiguration(locations={"classpath*:spring/*-config.xml"})
    @RunWith(SpringJUnit4ClassRunner.class)
    @TransactionConfiguration(transactionManager="transactionManager", defaultRollback=true)
    @Transactional(readOnly=true)
    public class AccountMapperTest {
      @Resource(name="accountMapper")
      AccountMapper accountMapper;
      
      @Test
      public void testGetByEmail() throws Exception {
        String email = "crexxx@gmail.com";
        Account account = accountMapper.getByEmail(email);
        
        System.out.println("account: " + account);
      }
      
      @Test
      @Rollback(true)
      @Transactional(readOnly=false)
      public void testInsert() throws Exception {
        String name = "ko";
        String email = "koxxx@gmail.com";
        
        Account account = new Account();
        account.setName(name);
        account.setEmail(email);
        
        accountMapper.insert(account);
        
      }
    }
    
    

Constants Utiltity class in spring core


스프링 프레임워크에 org.springframework.core.Constants 라는 클래스를 우연히 보게 되었다.

다른 클래스의 상수 값을 읽어내는 유틸리티 클래스다.
package spring.core;

public class MyConstants {

  public static final int SIZE = 10;
  public static final String HOSTS_PATH = "/etc/hosts";
 
  public static final String DB_TABLE_USER = "tbl_user";
  public static final String DB_TABLE_MENU = "tbl_menu";
  public static final String DB_TABLE_AUTH = "tbl_auth";
 
}


위와 같이 상수를 정의한 클래스가 있다고 하자.
org.springframework.core.Constants 클래스는 생성자로 상수가 정의되어 있는 클래스를 인자로 받는다.
숫자와 문자열로 정의된 상수값을 읽어 올 수 있다.
또한 prefix를 기준으로 상수를 그루핑할 수 있다. MyConstants 클래스에서는 'DB_TABLE_' 문자열을 prefix로 사용할 수 있다.

잘못된 상수명을 사용한다면 리턴 값이 null이 아니라 org.springframework.core.ConstantException 이 throw 된다는 점은 유의해야 한다.

Constants constants = new Constants(MyConstants.class);


숫자 값을 읽을 때는 아래와 같이 사용한다.
Number number = constants.asNumber("SIZE");

문자열 값을 읽을 때는 아래와 같이 사용한다.
String hostsPath = constants.asString("HOSTS_PATH");

상수명을 그룹핑 할 때는 아래와 같이 사용한다.
Set names = constants.getNames("DB_TABLE_");

package spring.core;

import java.util.Set;


import org.junit.Assert;
import org.junit.Test;
import org.springframework.core.Constants;

public class ConstantsTest {

 @Test
 public void testConstants() throws Exception {
  Constants constants = new Constants(MyConstants.class);
  
  String hostsPath = constants.asString("HOSTS_PATH");
  System.out.println(hostsPath);
  Assert.assertEquals(MyConstants.HOSTS_PATH, hostsPath);
  
  Number number = constants.asNumber("SIZE");
  System.out.println("number: " + number);
  Assert.assertEquals(MyConstants.SIZE, number.intValue());
  
  Set names = null;
  
  names = constants.getNames("DB_TABLE_");
  System.out.println(names);
  Assert.assertEquals(3, names.size());

  
  names = constants.getNamesForProperty("dbTable");
  System.out.println(names);
  Assert.assertEquals(3, names.size());
 }

}