현재 운영하는 시스템은 로깅 라이브러리로 log4j를 사용하고 있다. 1달 예상으로 신규 기능을 추가하는 작업을 진행하고 있는데, 이참에 log4j를 logback으로 전환하는 작업을 진행하였다.


logback이란?


logback은 log4j를 만든 개발자가 log4j를 토대로 새롭게 만든 Logging 라이브러리다.


우선 logback 사용시 얻게 되는 몇가지 장점에 대해 알아보자.

  • log4j보다 약 10배 정도 빠르게 수행되도록 내부가 변경되었으며, 메모리 효율성도 좋아졌다.
  • log4j때부터 광범위한 테스트를 진행한 경험을 가지고 있으며, logback은 더욱 높은 레벨의 테스트를 통해 검증되었다.
  • 문서화가 잘 되어 있다.
  • 설정 파일을 변경하였을 경우, 서버 재기동 없이 변경 내용이 자동으로 갱신된다.
  • 서버 중지 없이 I/O Faliure에 대한 복구를 지원한다.
  • RollingFileAppender를 사용할 경우 자동적으로 오래된 로그를 지워주며 Rolling 백업을 처리한다. (log4j를 사용했을땐 이 기능이 없어서 오래된 로그를 쉘 스크립트로 수동으로 삭제했었다.)
추가적인 내용은 logback 홈페이지(http://logback.qos.ch/reasonsToSwitch.html)에서 확인할 수 있다.


logback과 slf4j


logback을 사용하기 위해선 slf4j(simple logging facade for java)를 함께 사용해야 한다. slf4j는 facade 패턴을 적용한 로깅 프레임워크다. facade 패턴은 인터페이스와 비슷한 역할이라 할 수 있다. slf4j와 함께 개발자는 원하는 로깅 라이브러리를 선택해 사용하면 된다. 따라서 slf4j를 사용한다면 개발하면서 로깅 라이브러리 변경이 필요한 경우 쉽게 교체가 가능하다.


logback과 slf4j 라이브러리 관계 다이어그램



 라이브러리명

 설명

 logback-core.jar

 logback 코어.

 logback-classic.jar

 slf4j에서 logback을 호출할 수 있도록 처리.

 jcl-over-slf4j.jar (선택)

 apache commons 로깅 -> slf4j 전환.

 log4j-over-slf4j.jar (선택)

 log4j 로깅 -> slf4j 전환.


jcl-over-slf4j.jar와 log4j-over-slf4j.jar 라이브러리는 기존에 사용하는 코드 또는 써드파티 라이브러리가 다른 로깅 라이브러리를 사용하고 있을 경우, 이를 전환해주는 플러그인 라이브러리다. 로깅 라이브러리를 변경했을 경우나 오픈소스 라이브러리를 사용할 경우 잘 챙겨야 하는 라이브러리다.


pom.xml 설정


총 5개의 라이브러리를 dependency로 추가한다.



appender 설정


resources 하위에 logback.xml 파일을 생성한다. 설정이 없을 경우 logback BasicConfigurator 기본설정 전략을 따른다.



적용시 유의할 점 


만약 기존에 logback이 아닌 log4j가 slf4j와 연동되어 있을 경우, 반드시 slf4j-log4j12.jar 라이브러리를 제거해야 한다. slf4j-log4j12.jar 라이브러리는 slf4j가 log4j에게 로깅 처리를 호출하는 라이브러리인데, 이 라이브러리가 포함되어 있을 경우 다음과 같은 이슈가 발생한다.

  • slf4j 멀티 바인딩 이슈 발생.
  • log4j-over-slf4j.jar가 포함되어 있을 경우, 순환 오류로 인한 StackOverflowError 발생.

이 문제점들은 써드파티 라이브러리 dependency 추가시에도 발생할 수 있다. 써드파티 라이브러리에 slf4j-log4j12.jar가 dependency가 되어 있을 수 있다. 이 경우 반드시 exclusion을 해줘야 한다.


http://stackoverflow.com/questions/31044619/slf4j-log4j12-vs-log4j-over-slf4j



참고자료



Posted by SungHoon, Park
,

API 통신을 할 때, 결과값 데이터형식이 XML일 경우 스프링의 Jaxb2Marshaller를 주로 사용하였다.

applicationContext에 빈을 설정하고, org.springframework.oxm패키지내에 있는 Unmarshaller 인터페이스에 해당 빈을 주입하여 unmarshal() 메소드를 통해 언마샬링을 하였다.

문제는 언마샬링을 할 때 대상이 되는 JAXB 모델중 @XmlRootElement에 name 속성이 같은 경우 Unmarshaller에서 어떤 모델로 인식해야 되는지 몰라 오류가 발생하는 것이였다.


소스는 다음과 같다.


[applicationContext.xml]


<bean id="marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
	<property name="packagesToScan">
		<list>
			<value>kr.co.pshcode.user.jaxb</value>
		</list>
	</property>
</bean>


[UserBOImpl.java]


import org.springframework.oxm.Unmarshaller;

@Slf4j
@Service
public class UserBOImpl implements UserBO {
	@Autowired
	private Unmarshaller unmarshaller;

	private String getUser(String userId) {
		User user = null;
		
		try {
			user = (User)unmarshaller.unmarshal(new StreamSource(makeUserApiUrl(userId, "PERSONAL")));
		} catch (Exception e) {
			log.error("user api error : {}\n", userId, e);
		}

		return user;
	}

	private String getGroupUser(String groupUserId) {
		GroupUser groupUser = null;
		
		try {
			groupUser = (GroupUser)unmarshaller.unmarshal(new StreamSource(makeUserApiUrl(groupUserId, "GROUP")));
		} catch (Exception e) {
			log.error("group user api error : {}\n", groupUserId, e);
		}

		return groupUser;
	}	
}


[서비스API XML]


<!-- 일반유저 --> <USER> <COMMON></COMMON> <DETAIL> <ID></ID> <NAME></NAME> <EMAIL></EMAIL> </DETAIL> </USER> <!-- 그룹유저 --> <USER> <COMMON></COMMON> <DETAIL> <GROUP_ID></GROUP_ID> <GROUP_NAME></GROUP_NAME> <GROUP_EMAIL></GROUP_EMAIL> </DETAIL> </USER>


[User.java, GroupUser.java]


@Data
@XmlRootElement(name = "USER")
@XmlAccessorType(XmlAccessType.FIELD)
public class User {
	@XmlElement(name = "COMMON")
	private UserCommon common;

	@XmlElement(name = "DETAIL")
	private UserDetail detail;
}

@Data
@XmlRootElement(name = "USER")
@XmlAccessorType(XmlAccessType.FIELD)
public class GroupUser {
	@XmlElement(name = "COMMON")
	private GroupUserCommon common;

	@XmlElement(name = "DETAIL")
	private GroupUserDetail detail;
}


사용자정보를 제공하는 API측에서는 XML의 루트를 <USER>로 일반유저, 그룹유저별로 <COMMON>과 <DETAIL>에 다른 엘리먼트명으로 각각 정보를 제공해주고 있다.

(비슷하지만 다른 느낌? 그런 느낌적인 느낌..)

이럴경우, Unmarshaller에서 정확한 JAXB 모델을 판단할 수 없기 때문에 언마샬링 오류가 발생하게 되는 것이다.


해결을 할 수 있는 간단한 방법으로는 2가지 케이스를 모두 언마샬링 할 수 있는 통합 모델을 만드는 것이다.


UserCommon + GroupUserCommon 합치기

UserDetail + GroupUserDetail 합치기


하지만 이렇게 처리할 경우 데이터를 객체지향적으로 관리할 수 없다. 

파싱할 XML 정보가 늘어날때마다 하나의 모델에 계속 추가를 하게 되면 결국 어떤 API에서만 활용되는 정보인지 구분이 안가게 된다. 그래서 이 방법은 Pass!


다른 방법은 Jaxb2Marshaller를 상속받아서 Class를 선택적으로 넘겨받을 수 있는 메소드를 만들어 기능을 확장하는 것이다. (unmarshal() 메소드 오버로딩)

Castor나 Jibx 같은 다른 OXM은 사용 안해봐서 설명하는 방법과 비슷한 방법이 있는지는 잘 모르겠다. ㅎㅎ


암튼 개선한 소스는 다음과 같다.


[개선된 CustomJaxb2Marshaller.java]


public class CustomJaxb2Marshaller extends Jaxb2Marshaller { public Object unmarshal(Source source, Class<?> clazz) throws JAXBException { Unmarshaller unmarshaller = createUnmarshaller(); return unmarshaller.unmarshal(source, clazz).getValue(); } }


Jaxb2Marshaller 클래스에 존재하는 createUnmarshaller() 메소드를 활용해서 javax.xml.bind 패키지의 Unmarshaller를 획득한 후, unmarshal(source, clazz) 메소드를 활용해서 선택적으로 파싱할 JAXB 모델 Class를 넘겨주어 언마샬링을 하는 것이다.


import kr.co.pshcode.oxm.CustomJaxb2Marshaller;

@Slf4j
@Service
public class UserBOImpl implements UserBO {
	@Autowired
	private CustomJaxb2Marshaller marshaller;

	private String getUser(String userId) {
		User user = null;
		
		try {
			user = (User)marshaller.unmarshal(new StreamSource(makeUserApiUrl(userId, "PERSONAL")), User.class);
		} catch (Exception e) {
			log.error("user api error : {}\n", userId, e);
		}

		return user;
	}

	private String getGroupUser(String groupUserId) {
		GroupUser groupUser = null;
		
		try {
			groupUser = (GroupUser)marshaller.unmarshal(new StreamSource(makeUserApiUrl(groupUserId, "GROUP")), GroupUser.class);
		} catch (Exception e) {
			log.error("group user api error : {}\n", groupUserId, e);
		}

		return groupUser;
	}	
}


이렇게 처리할 경우, 기존의 Jaxb2Marshaller를 활용하면서 @XmlRootElement에 name속성이 같을 경우도 커버할 수 있어 문제를 해결할 수 있다. ^^;




Posted by SungHoon, Park
,

스프링 동적리스트 바인딩의 경우 최대 256개까지가 기본설정으로 되어 있다.

만약, 256개 이상을 파라미터로 넘기게 된다면 IndexOutOfBoundsException이 발생하게 될 것이다.


해결방법은 스프링 InitBinder의 기본설정 값을 변경해준다.

(LazyList를 이용하여 256개를 확장하는 방법도 있는 것 같은데, 이 방법은 안써봐서 패스~)


@Controller
public class TestController {
	private static final int AUTO_GROW_COLLECTION_LIMIT = 500;

	@InitBinder
	public void initBinder(WebDataBinder dataBinder) {
		dataBinder.setAutoGrowCollectionLimit(AUTO_GROW_COLLECTION_LIMIT);
	}
}


그리고 톰캣쪽 maxParameterCount값도 한번 확인해 보는 것이 좋다.

필자는 최대 500개의 리스트를 바인딩하기 위해 위와 같은 설정을 하였는데, 변경하고도 계속 다음과 같은 오류가 발생했다.


경고: More than the maximum number of request parameters (GET plus POST) for a single request ([10,000]) were detected. Any parameters beyond this limit have been ignored. To change this limit, set the maxParameterCount attribute on the Connector.


톰캣은 maxParameterCount값이 기본으로 10000개가 설정되어 있는데, 이 이상이 초과가 되면  에러가 발생함.

그래서 아래와 같이 -1 무제한으로 변경.

정확한 최대 파라미터 갯수가 측정될 경우엔 숫자를 써주는 것이 더 좋을듯하다.


<Connector port="8080" protocol="HTTP/1.1" 
               connectionTimeout="20000" 
               redirectPort="8443"
               URIEncoding="UTF-8"
               maxParameterCount="-1"/>



Posted by SungHoon, Park
,