'java'에 해당되는 글 29건

  1. Spring, RestTemplate으로 REST 클라이언트 구현하기
  2. Spring Boot, @Async 비동기 실행 로직 구현하기
  3. Spring Boot, JUnit을 이용한 유닛 테스트 구현하기
  4. Java, Thumbnailator를 이용하여 이미지 썸네일 생성(변환)하기
  5. Spring Boot, MyBatis 연동으로 MySQL 데이터베이스 질의하기 (4)
  6. Java에서 User-Agent 파써 사용하기
  7. Jersey 2, JAX-RS + MVC 적용하기
  8. CentOS에서 Apache Tomcat 설치하기
  9. Apache Tomcat, java.lang.OutOfMemoryError: Java heap space 오류 발생시 대처 방법
  10. 서버-클라이언트(Java-JavaScript)간 날짜/시간 데이터 다루기

Spring, RestTemplate으로 REST 클라이언트 구현하기

개요

웹의 시대가 열린지는 오래되었지만 REST API가 본격적으로 유행한지는 얼마 되지 않았다. 안타깝게도 국내는 비즈니스 로직 구현에만 초점을 맞추어 GET, POST 메써드의 구분 없이 API 요청을 허용한다거나(심지어 POST 요청에 쿼리 스트링을 담기도 한다.) 상태 코드를 사용하지 않고 무조건 200 응답 후 바디에 독자적인 코드를 재정의하는 등 HTTP 스펙의 권고사항을 무시한 API 개발이 너무나도 흔한 상황이다. 그럼에도 REST는 가독성, 유지보수성 등을 고려했을 때 꼭 필요한 설계 철학이다.(세계 최대의 커뮤니티 reddit 또한 REST API를 제공한다.) 이번 글에서는 이러한 REST API를 이용(소비)하는 클라이언트로서 RestTemplate의 사용 예를 설명하고자 한다.

라이브러리 종속성 추가

dependencies {
    compile group: 'org.springframework', name: 'spring-web', version: '4.2.6.RELEASE'
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.7.4'
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.7.4'
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.7.4'
    compile group: 'org.projectlombok', name: 'lombok', version: '1.16.8'
}
  • RestTemplate를 사용하기 위해 spring-web 아티팩트를 추가하였다.

  • RestTemplatePOJO-JSON 상호 변환시 기본값으로 Jackson 라이브러리를 사용한다. Jackson이 정상적으로 동작하려면 jackson-core, jackson-databind, jackson-annotations 3개 아티팩트를 추가해야 한다.

  • 만약 Jackson을 추가하지 않고 Gson을 추가하면 RestTemplateGson을 대신 사용한다. 어떤 HttpMessageConverter 구현 클래스를 등록하느냐의 차이로 MappingJackson2HttpMessageConverter, GsonHttpMessageConverter가 이 역할을 수행하며 RestTemplate 오브젝트 생성시 생성자로 명시적인 전달이 가능하다.

  • 만약 Spring Boot 기반의 프로젝트로 spring-boot-starter-web 아티팩트를 추가한 상태라면 위에 열거한 아티팩트를 추가할 필요가 없다. 모두 포함되어 있기 때문이다.(Gson은 따로 추가해야 한다.)

  • lombok 아티팩트 추가는 필수가 아닌 선택이다. LombokPOJO 작성시 반복적이고 지루한 RequiredArgsConstructor, Getter, Setter, ToString 메써드 작성을 자동화 해준다.(적지 않은 수의 국내 개발자들 이 작업이 귀찮아 POJO 대신 Map을 사용하는데 이는 Java가 가진 많은 장점을 포기하는 것이다.)

HTML GET 요청

단순한 HTML 응답을 받는 예이다.

HttpHeaders header = new HttpHeaders();
header.add(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE);
ResponseEntity<String> response = new RestTemplate().exchange("http://www.naver.com/", HttpMethod.GET, new HttpEntity(header), String.class);

JSON GET 요청

에코 응답을 제공하는 JSON Test API를 사용하여 예를 작성해보겠다. JSON GET 요청에 앞서 응답을 받을 POJO 클래스를 구현한다. 아래 HeadersResponse 클래스는 JSON 응답을 담는 역할을 한다.

@Data
public class HeadersResponse {

    @JsonProperty("Accept-Language")
    private String acceptLanguage;

    @JsonProperty("Host")
    private String host;

    @JsonProperty("User-Agent")
    private String userAgent;

    @JsonProperty("Accept")
    private String accept;
}
  • 앞서 언급한 Lombok을 이용하여 POJO 구현시 반복적인 메써드의 작성을 제거했다. 클래스 레벨에 @Data를 명시하면 모든 것을 컴파일 시점에 대신 생성해준다.
  • @JsonPropertyPOJO-JSON 상호변환시 정확한 멤버 변수에 맵핑해주는 역할을 한다. Jackson이 제공하는 기능으로 Gson을 사용한다면 Gson이 제공하는 기능을 사용해야 한다.

작성된 POJO을 이용한 요청 예이다.

HttpHeaders header = new HttpHeaders();
header.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_UTF8_VALUE);
ResponseEntity<HeadersResponse> response = new RestTemplate().exchange("http://headers.jsontest.com/", HttpMethod.GET, new HttpEntity(header), HeadersResponse.class);

참고 글

저작자 표시 비영리 동일 조건 변경 허락
신고

Spring Boot, @Async 비동기 실행 로직 구현하기

먼저 읽어볼만한 글

@SpringBootApplication 클래스 작성

package com.jsonobject.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@SpringBootApplication
@EnableAutoConfiguration
@EnableAsync
public class Application {

    public static void main(String[] args) {

        ApplicationContext ctx = SpringApplication.run(Application.class, args);
    }

    @Bean
    public TaskExecutor taskExecutor() {

        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(10);
        taskExecutor.setMaxPoolSize(20);
        taskExecutor.setQueueCapacity(50);

        return taskExecutor;
    }
}
  • @EnableAsync를 명시함으로서 비동기로 작동할 @Async가 명시된 빈을 인식하도록 한다.

@Service 클래스 작성

package com.jsonobject.example;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class AsyncService {

    @Async
    public void doAsync() {
        // 비동기로 실행될 로직을 작성
    }
}
  • Spring Boot 기반의 웹 애플리케이션은 HTTP 요청이 들어왔을 때 내장된 서블릿 컨테이너(Tomcat)가 관리하는 독립적인 1개의 Worker 쓰레드에 의해 동기 방식으로 실행된다. 하지만 요청 처리 중 @Async가 명시된 메써드를 호출하면 앞서 등록한 ThreadPoolTaskExecutor 빈에 의해 관리되는 또 다른 독립적인 Worker 쓰레드로 실행된다. 별도의 쓰레드로 동작하기에 원래의 요청 쓰레드는 그대로 다음 문장을 실행하여 HTTP 응답을 마칠 수 있다.

  • @Async가 명시된 메써드는 반드시 public으로 선언되어야 한다. 또한 같은 클래스 내에서 해당 메써드를 호출할 경우 비동기로 작동하지 않는다.

참고 글

저작자 표시 비영리 동일 조건 변경 허락
신고

Spring Boot, JUnit을 이용한 유닛 테스트 구현하기

먼저 읽어볼만한 글

라이브러리 종속성 추가

/build.gradle 파일에 아래 내용을 추가한다.

dependencies {
    testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test'
}
  • spring-boot-starter-test Starter POM의 추가 만으로 Spring Test, JUnit, Hamcrest, Mockito를 모두 사용하여 테스트 클래스를 작성할 수 있다.

테스트 클래스 작성

/src/test/java/com.jsonobject.example/ExampleTest.java 파일을 아래와 같이 작성한다.

import com.jsonobject.example.Application;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebIntegrationTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@ActiveProfiles(profiles = "test")
public class ExampleTest {

    @Autowired
    @Qualifier("anotherDAO")
    private SomeDAO someDAO;

    @Resource(name = "anotherService")
    private SomeService someService;

    @Autowired
    private SomeController someController;

    @Test
    public void aTest() {
        // 테스트 코드 작성
    }

    @Test
    public void bTest() {
        // 테스트 코드 작성
    }
}
  • 클래스에 @SpringApplicationConfiguration를 명시하면 ApplicationContext를 선택하여 테스트를 할 수 있다. 이에 따라 클래스 멤버에 애플리케이션 컨텍스트의 빈을 주입하여 테스트에 사용할 수 있다. @Autowired, @Qualifier, @Resource 모두 사용 가능하다.

  • 클래스에 @WebIntegrationTest를 명시하면 실제 웹 애플리케이션이 구동되는 환경과 동일하게 HTTP 요청 테스트가 가능하다. TestRestTemplate 클래스를 사용한다.

  • 클래스에 @ActiveProfiles를 명시하면 테스트 실행시 적용될 프로파일을 적용할 수 있다.

  • 메써드에 @Test를 명시하면 테스트 메써드로 선언되어 메써드 단위 테스트가 가능해진다. 클래스 단위 테스트시 실행될 대상 메써드가 된다.

  • JUnit에서는 클래스 단위 테스트 실행시 메써드 간의 테스트 순서가 임의로 진행된다. 클래스에 @FixMethodOrder를 명시하면 테스트 메써드 간의 실행 순서를 결정할 수 있다. MethodSorters.NAME_ASCENDING은 메써드의 이름 순으로 실행 순서를 결정하겠다는 의미이다.

참고 글

저작자 표시 비영리 동일 조건 변경 허락
신고

Java, Thumbnailator를 이용하여 이미지 썸네일 생성(변환)하기

개요

Java 진영은 javax.imageio 패키지명으로 제공되는 Java Image I/O API를 이용하여 리사이즈, 크롭 등의 이미지 처리를 할 수 있다. 이를 이용하여 Java 6 이상의 경우 웹에서 가장 많이 사용되는 JPEG, PNG, GIF(BMP 포함) 이미지를 자유롭게 처리할 수 있다. 하지만 단순히 이미지에 대한 썸네일만 하고 싶다면? Image IO의 사용법은 너무 번거롭다. 이번 글에서는 블로그 글로 쓰기 민망할 정도로 사용법이 쉬운 이미지 썸네일 생성 라이브러리인 Thumbnailator를 소개하고자 한다.

라이브러리 의존성 추가

Gradle 빌드 환경이라면 /build.gradle 파일에 아래와 같이 Thumbnailator 라이브러리를 추가한다.

dependencies {
    compile group: 'net.coobird', name: 'thumbnailator', version: '0.4.8'
}

Maven 빌드 환경이라면 /pom.xml 파일에 아래와 같이 추가한다.

<dependency>
    <groupId>net.coobird</groupId>
    <artifactId>thumbnailator</artifactId>
    <version>0.4.8</version>
</dependency>

이미지 썸네일 생성하기

이미지 썸네일을 생성하는 방법은 아래와 같다. 황당할 정도로 더 이상의 설명이 필요 없을 정도이다. 놀라운 것은 빠른 속도로 고품질의 썸네일을 생성한다는 것이다.

File image = new File("D:/image.jpg");
File thumbnail = new File("D:/thumbnail.png");
if (image.exists()) {
    thumbnail.getParentFile().mkdirs();
    Thumbnails.of(image).size(190, 150).outputFormat("png").toFile(thumbnail);
}

참고 글

저작자 표시 비영리 동일 조건 변경 허락
신고

Spring Boot, MyBatis 연동으로 MySQL 데이터베이스 질의하기

먼저 읽어볼만한 글

데이터베이스 연결 및 질의를 위한 의존성 정보 추가

/build.gradle에 아래 내용을 추가한다. MyBatis 라이브러리를 이용하여 MySQL 데이터베이스에 연결하기 위한 의존성 정보이다.

dependencies {
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-jdbc', version: '1.3.5.RELEASE'
    compile group: 'org.mybatis.spring.boot', name: 'mybatis-spring-boot-starter', version: '1.1.1'
    compile group: 'org.mybatis', name: 'mybatis-typehandlers-jsr310', version: '1.0.1'
    compile group: 'mysql', name: 'mysql-connector-java', version: '6.0.2'
}
  • mybatis-typehandlers-jsr310 아티팩트는 데이터베이스 컬럼의 날짜/시간 타입을 Java 8부터 추가된 LocalDate, LocalTime, LocalDateTime, Year, Month 클래스로의 자동 맵핑을 지원한다. MyBatis 3.4 버전부터 자동 지원되며 이전 버전은 별도의 typeHandler 등록이 필요하다.

데이터베이스 연결 정보 작성

/src/main/resouces/application.properties에 아래와 같이 연결한 데이터베이스 연결 정보를 작성한다. Profile에 따라 다른 연결 정보를 작성하면 개발, 운영 환경에 따라 연결 데이터베이스를 다르게 설정할 수 있다.

spring.datasource.type=org.apache.tomcat.jdbc.pool.DataSource
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/spring_boot_example
spring.datasource.username=root
spring.datasource.password=Q1w2e3r4
spring.datasource.connectionProperties=useSSL=false;useUnicode=yes;characterEncoding=UTF-8;serverTimezone=UTC
spring.datasource.initSQL=SELECT 1

  • DataSourceProperties 클래스에 의해 위에 명시한 데이터베이스 연결 정보를 맵핑한다.


    • spring.datasource.type: 데이터베이스에 연결할 DataSource 클래스명을 명시한다. 여기서는 Tomcat JDBC Connection Pool을 명시했다. 별도로 명시하지 않을 경우 DataSourceBuilder 클래스에 의해 Tomcat JDBC Connection Pool, HikariCP, DBCP 1, DBCP 2 순서대로 유무를 조사하여 자동 등록한다.

    • spring.datasource.driverClassName: 데이터베이스에 연결할 Driver 클래스명을 명시한다. MySQL에 접속하기 위해 Connector/J를 명시했다. 역시 별도로 명시하지 않을 경우 자동 등록한다.


  • Tomcat JDBC Connection Pool을 사용할 경우 PoolConfiguration 클래스에 의해 추가 설정을 적용한다. 위 예제에는 2개의 추가 설정 만을 명시했지만 실제로 레퍼런스 문서에 명시된 모든 설정 값을 명시할 수 있다.


    • spring.datasource.connectionProperties: 데이터베이스 연결시 적용할 파라메터를 명시한다. 데이터베이스 환경마다 다르므로 적절한 파라메터를 입력한다.


    • spring.datasource.initSQL: 새로운 연결을 생성할 때 마다 실행할 SQL 문을 명시한다.


  • DataSourceAutoConfiguration 클래스에 의해 위 맵핑된 데이터베이스 연결 정보로 dataSource(javax.sql.DataSource) 이름의 빈을 생성한다. 위 방법을 사용하지 않고 직접 입맛에 맞게 dataSource 빈을 생성해도 무방하다.


  • MybatisAutoConfiguration 클래스에 의해 위 생성된 dataSource 빈을 자동으로 인지하여 sqlSeesionFactory(org.mybatis.spring), sqlSessionTemplate(org.mybatis.spring) 이름의 빈을 생성한다. 이 것 만으로 애플리케이션에서 MySQL 데이터베이스에 질의하기 위한 준비는 끝났다.

데이터베이스 질의를 수행하는 DAO 클래스 작성

/src/main/com.jsonobject.example/ExampleDAO.java를 아래와 같이 작성한다. 데이터베이스의 현재 시간을 문자열로 반환하는 간단한 기능이다. 아래와 같은 어노테이션 기반의 질의문 작성은 간단하고 직관적이지만 실제로 프로덕션 레벨에서 요구되는 Dynamic SQL을 소화하기에는 한계가 있으므로 XML 방식의 Mapper 사용을 추천한다.

package com.jsonobject.example;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface ExampleDAO {

    @Select("SELECT NOW()")
    String getCurrentDateTime();
}

DAO를 소비하는 비즈니스 로직 작성

아래는 앞서 작성한 DAO 클래스를 이용하여 비즈니스 로직을 작성한 예이다. 기본적인 Java, JDBC를 이용한 질의에 비해 Spring Boot, MyBatis를 이용한 질의는 매우 간단한 것을 확인할 수 있다.

package com.jsonobject.example;

@Service
public class ExampleService {

    @Autowired
    private ExampleDAO exampleDAO;

    public String getCurrentDataTime() {

        return exampleDAO.getCurrentDateTime();
    }
}

2개 이상의 데이터베이스 연결을 위한 설정

앞서 설명한 방법은 애플리케이션에서 1개의 데이터베이스에만 연결한다는 전제 하에 Spring Boot, MyBatis에 의해 미리 만들어진 코드에 의해 각 1개의 DataSource, SqlSessionFactory, SqlSessionTemplate 빈을 자동으로 생성하고 사용하는 시나리오였다. 하지만 프로덕션 레벨에서는 2개 이상의 데이터베이스에 연결하는 요구사항이 흔히 발생한다. 이번에는 2개 이상의 데이터베이스 질의를 수용할 수 있는 방법을 설명한다. 연결할 데이터베이스의 이름을 first라고 가정하고 진행하겠다.

/src/main/resources/application.properties 파일을 아래와 같이 작성한다.

spring.first.datasource.type=org.apache.tomcat.jdbc.pool.DataSource
spring.first.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.first.datasource.url=jdbc:mysql://localhost:3306/spring_boot_example
spring.first.datasource.username=root
spring.first.datasource.password=Q1w2e3r4
spring.first.datasource.connectionProperties=useSSL=false;useUnicode=yes;characterEncoding=UTF-8;serverTimezone=UTC
spring.first.datasource.initSQL=SELECT 1
  • first라는 키워드를 부여함으로서 추후 second, third 등 2개 이상의 데이터베이스 연결이 가능하도록 확장성을 부여했다. 단지 예시일 뿐 어떤 이름을 부여해도 무방하다.

/src/main/java/com.jsonobject.example/FirstDataSoureConfig.java 파일을 아래와 같이 작성한다.

package com.jsonobject.example;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class FirstDataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.first.datasource")
    public DataSource firstDataSource() {

        return DataSourceBuilder.create().build();
    }

    @Bean
    public SqlSessionFactory firstSqlSessionFactory(DataSource firstDataSource, ApplicationContext applicationContext) throws Exception {

        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(firstDataSource);
        sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath:mapper/first/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }

    @Bean
    public SqlSessionTemplate firstSqlSessionTemplate(SqlSessionFactory firstSqlSessionFactory) throws Exception {

        return new SqlSessionTemplate(firstSqlSessionFactory);
    }
}
  • 앞서 1개의 데이터베이스 만을 구성할 때는 별도의 클래스 작성이 필요 없었지만 2개 이상부터는 위와 같이 DataSource, SqlSessionFactory, SqlSessionTemplate 빈을 별도로 생성하는 작업이 필요하다. 다른 데이터베이스 연결을 위한 작업시 위와 동일한 형태의 클래스를 작성해주면 된다.

/src/main/java/com.jsonobject.example/ExampleDAO.java 파일을 아래와 같이 작성한다.

package com.jsonobject.example;

import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Repository;

@Repository
public class ExampleDAO {

    @Autowired
    @Qualifier("firstSqlSessionTemplate")
    private SqlSession sqlSession;

    public String getCurrentDateTime() {

        return sqlSession.selectOne("com.jsonobject.example.mapper.ExampleMapper.getCurrentDateTime");
    }
}
  • 앞서 생성한 firstSqlSessionTemplate 빈을 맵핑한 것을 확인할 수 있다.

마지막으로 /src/main/resources/mapper/first/ExampleMapper.xml 파일을 아래와 같이 작성한다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.jsonobject.example.mapper.ExampleMapper">
    <select id="getCurrentDateTime" resultType="String">
        SELECT NOW()
    </select>
</mapper>
  • MyBatis의 장기인 강력한 Dynamic SQL 기능을 이용하려면 위와 같이 Mapper라 불리는 XML 문서에 SQL 문을 작성해야 한다. 프로젝트의 통일성을 위해 DAO 클래스의 패키지명과 일치하는 경로에 저장하도록 한다. 예를 들면 아래와 같다.
    • /src/main/java/com/jsonobject/example/dao/UserDAO.java
    • /src/main/resources/com/jsonobject/example/mapper/UserMapper.xml

참고 글


저작자 표시 비영리 동일 조건 변경 허락
신고

Java에서 User-Agent 파써 사용하기

개요

User-Agent는 HTTP 요청시 헤더에 담겨 오는 정보로 요청 클라이언트의 디바이스 및 브라우저 정보를 담고 있다. 국제적으로 표준화되어 있지 않아 엄격히 관리되고 있지 않으며 어떤 값을 넣어도 상관없기 때문에 완전히 신뢰할 수 있는 데이터는 아니지만 간단한 통계 및 디바이스 특성에 따른 분기 처리에 있어 가장 기초가 되는 데이터이기도 하다.

Java 진영의 User-Agent 파써 도구들

User-Agent 파써의 관건은 자체적으로 보유한 데이터베이스의 양이 중요하다. 현재 이순간에도 실시간으로 새로운 User-Agent 정보가 추가되고 있기 때문이다. 데이터베이스가 주기적으로 갱신되지 않으면 새로운 기기, 새로운 브라우저에 대한 감지가 되지 않을 가능성이 높다.

  • user-agent-utils: Java 진영 최초의 User-Agent 파써이다. 현재 개발 중단되었다.

  • UADetector: Java 진영에서 가장 잘 만들어진 User-Agent 파써이다. 아쉬운 점은 2014-10 부로 User-Agent 데이터베이스의 업데이트가 중단되었다. 본 글에서 사용할 라이브러리이다.

  • uap-java: 사용자들의 참여로 현재까지 계속 User-Agent 데이터베이스가 업데이트되고 있는 uap-core 기반의 파써이다. 데이터베이스의 주기적 갱신이라는 측면에서 가장 높은 점수를 주고 싶다.

UADetector, 의존성 추가

본 글에서 소개할 파써는 UADetector이다. 먼저 /build.gradle 파일에 아래와 같이 라이브러리 의존성을 추가한다.

dependencies {
    compile group: 'net.sf.uadetector', name: 'uadetector-core', version: '0.9.22'
    compile group: 'net.sf.uadetector', name: 'uadetector-resources', version: '2014.10'
}

사용 예

String userAgent = "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36";

// User-Agent 데이터베이스를 로드하여 파써를 초기화한다. 초기화는 소모적인 작업이므로 Spring 환경의 경우 Bean으로 관리하는 것을 추천한다.
UserAgentStringParser parser = UADetectorServiceFactory.getResourceModuleParser();

// ReadableDevieCategory.Category를 반환한다.
// PERSONAL_COMPUTER, SMARTPHONE, TABLET, SMART_TV, WEARABLE_COMPUTER, GAME_CONSOLE, PDA, OTHER, UNKNOWN을 반환한다.
parser.parse(userAgent).getDeviceCategory().getCategory();

// ReadableOperatingSystemFamily.OperatingSystemFamily을 반환한다.
// WINDOWS, MAC_OS, BSD, LINUX, ANDROID, IOS, BLACKBERRY_OS, UNKNOWN 등을 반환한다.
parser.parse(userAgent).getOperatingSystem().getFamily();

// UserAgentType을 반환한다.
// BROWSER, MOBILE_BROWSER, OFFLINE_BROWSER, ROBOT, LIBRARY, UNKNOWN 등을 반환한다.
parser.parse(userAgent).getType();

// UserAgentFamily를 반환한다.
// CHROME, CHROME_MOBILE, FIREFOX, MOBILE_FIREFOX, SAFARI, MOBILE_SAFARI, IE, IE_MOBILE, UNKNOWN 등을 반환한다.
parser.parse(userAgent).getFamily();
저작자 표시 비영리 동일 조건 변경 허락
신고

Jersey 2, JAX-RS + MVC 적용하기

개요

최근 회사에서 N 포탈과의 제휴 서비스를 Jersey 2로 개발하면서 생산성 측면에서 쏠쏠한 재미를 보았다. 이번 글은 Jersey 2 프레임워크에서 MVC 패턴을 적용하는 방법을 간단히 설명하고자 한다.

Jersey란 무엇인가?

Jersey 2Oracle이 직접 JAX-RS 표준을 구현한 공식 레퍼런스 구현체로 Java 플랫폼에서 RESTful Web Services를 높은 생산성으로 구현하고 소비할 수 있게 해주는 프레임워크이다.(JAX-RS 표준이 서버만을 고려하여 작성된 데 반해 Jersey 2는 클라이언트 입장에서도 서비스를 편리하게 소비할 수 있도록 구현되었다.)

MVC 패턴을 적용해보자

Jersey 2는 기본적으로 RESTful Web Services에 초점을 맞추고 있지만 공식적으로 제공되는 jerser-mvc-jsp 확장 모듈을 사용하여 국내 Java 웹 개발의 표준이나 다름 없는 Spring MVC 만큼이나 직관적이고 편리한 MVC 기능을 사용할 수 있다. Maven 기반의 프로젝트임을 가정하고 MVC 패턴 적용 방법을 차례로 설명한다. Jersey 2 기본 프로젝트 생성은 이 글을 참고한다.

/pom.xml

<dependency>
    <groupId>org.glassfish.jersey.ext</groupId>
    <artifactId>jersey-mvc-jsp</artifactId>
    <version>2.19</version>
</dependency>
<dependency>
    <groupId>taglibs</groupId>
    <artifactId>standard</artifactId>
    <version>1.1.2</version>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>
  • jersey-mvc-jsp 라이브러리는 MVC 기능을 제공하는 Jersey 2의 확장 모듈이다.
  • JSP 파일 작성시 JSTL 표준 템플릿 태그를 사용하기 위해 standard, jstl 라이브러리를 추가하였다.

/src/main/java/.../AppConfig.java

@ApplicationPath("/")
public class AppConfig extends ResourceConfig {

    public AppConfig() {

        packages(this.getClass().getPackage().getName());
        register(JspMvcFeature.class);
    }
}
  • 웹 애플리케이션 기동시 org.glassfish.jersey.servlet.ServletContainer에 읽혀질 환경 설정 파일이다. JspMvcFeature.class를 등록하여 MVC 기능을 활성화한다.
  • @ApplicationPath("/") 명시는 / 경로(ContextPath)에 대한 모든 요청시 이 환경설정이 적용된다는 것을 의미한다.

/src/main/webapp/WEB-INF/web.xml

<filter>
    <filter-name>jersey-mvc-filter</filter-name>
    <filter-class>org.glassfish.jersey.servlet.ServletContainer</filter-class>
    <init-param>
        <param-name>javax.ws.rs.Application</param-name>
        <param-value>com.jsonobject.sample.AppConfig</param-value>
    </init-param>
    <init-param>
        <param-name>jersey.config.servlet.filter.contextPath</param-name>
        <param-value></param-value>
    </init-param>
    <init-param>
        <param-name>jersey.config.server.mvc.templateBasePath.jsp</param-name>
        <param-value>/WEB-INF/jsp</param-value>
    </init-param>
    <init-param>
        <param-name>jersey.config.servlet.filter.staticContentRegex</param-name>
        <param-value>/assets/.*</param-value>
    </init-param>
    </filter>
<filter-mapping>
    <filter-name>jersey-mvc-filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
  • Servlet 3.x 기반의 프로젝트라면 web.xml에 별도로 Jersey 2ServletContainer 클래스를 등록하지 않아도 된다. 그럼에도 위와 같이 FilterServletContainer를 등록한 것은 이미지, JavaScript, CSS와 같은 정적인 파일(Static Assets)을 처리하기 위함이다.
  • jersey.config.server.mvc.templateBasePath.jsp에 대한 값으로 /WEB-INF/jsp 경로를 명시하였다. MVC 패턴의 View로서 기능하는 JSP 파일은 모두 /src/main/webapp/WEB-INF/jsp 폴더에 위치한다는 의미이다.
  • jersey.config.servlet.filter.staticContentRegex에 대한 값으로 /assets/.* 정규식을 명시하였다. /assets/css/some.css와 같은 정적 파일 요청시 특정 Resource 클래스로 분기하지 않고 바로 처리하겠다는 의미이다.

/src/main/java/.../HelloResource.java

@Path("/")
public class HelloResource {

    @GET
    @Path("helloworld")
    @Produces(MediaType.TEXT_HTML)
    public Viewable getSource() {
        Map<String, Object> model = new HashMap<String, Object>();
        model.put("title", "Hello!");
        model.put("message", "Hello, World!");
        return new Viewable("/helloworld.jsp", model);
    }
}
  • Viewable 클래스는 MVC 패턴의 View 기능을 하는, Jersey 2가 독자적으로 제공하는 클래스이다. Spring MVC에 익숙한 개발자라면 ModelAndView 클래스와 유사한 것을 느낄 것이다. 클라이언트의 요청에 대해 처리한 결과를 Model 오브젝트에 담아 Viewable 오브젝트에 전달하여 동적인 View를 생성한다.

/src/main/webapp/WEB-INF/jsp/helloworld.jsp

<%@ page language="java" contentType="text/html" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <title>${it.title}</title>
</head>
<body>
${it.message}
</body>
</html>
  • 마지막 단계이다. 앞서 전달 받은 Model 오브젝트는 ${it.modelName}과 같은 형태로 View에 맵핑되어 클라이언트에 응답으로 반환된다.

결론

최근 해외에서는 Python 진영의 Flask 프레임워크가 웹 개발시 높은 생산성으로 많은 인기를 얻고 있다. Java 진영에도 Flask와 대적할만한 Dropwizard 프레임워크가 있다. DropwizardJava 진영의 여러 유용한 라이브러리를 엄선, 짜집기하여 만든 종합선물세트이자 맥가이버 칼과도 같은 프레임워크라고 볼 수 있다. 흥미로운 점은 JAX-RS 표준을 구현한 많은 프레임워크 중에 Jersey 2DropwizardREST 담당 모듈로 선정되었다는 것이다. 그만큼 성능을 인정 받은 것이라고 평가할 수 있다. 엄격한 표준 구현, 높은 생산성 측면에서 Spring MVC에만 익숙했던 국내 개발자들에게 Jersey 2MVC의 조합을 추천한다.

참고 문서

저작자 표시 비영리 동일 조건 변경 허락
신고

CentOS에서 Apache Tomcat 설치하기

개요

Apache TomcatJava EE 스펙의 부분 구현체로 소규모 웹사이트부터 엔터프라이즈 웹 애플리케이션까지 광범위하게 사용되는 가볍고 빠른 성능의 오픈 소스 웹 컨테이너이다. 이번 글에서는 CentOS 6에서 Apache Tomcat 8을 설치하고 실행하는 과정을 설명해보고자 한다.

방화벽 허용하기

$ vi /etc/sysconfig/iptables
iptables -A INPUT -m state --state NEW -p tcp --dport 8080 -j ACCEPT

$ service iptables restart
  • Apache Tomcat의 기본 서비스 포트는 8080이다. 해당 포트를 방화벽에서 허용하도록 설정한 후 방화벽을 재시작한다.

사용자 계정 생성하기

$ groupadd tomcat
$ useradd -g tomcat tomcat
$ passwd tomcat
  • root 계정으로 Apache Tomcat을 바로 실행하는 것은 보안상 피해야 한다. 설치에 앞서 Tomcat을 관리하기 위한 tomcat이라는 이름의 그룹 및 사용자 계정을 새로 생성한다.

JDK 설치하기

$ su - tomcat
$ wget http://download.oracle.com/otn-pub/java/jdk/7u71-b14/jdk-7u71-linux-x64.tar.gz
$ tar -zxvf jdk-7u71-linux-x64.tar.gz
$ rm jdk-7u71-linux-x64.tar.gz
$ ln -s jdk1.7.0_71 jdk
  • Apache TomcatJava에 종속적이다. CentOSyum을 이용하여 손쉽게 OpenJDK를 설치할 수 있다. 만약 시스템에 설치된 JDK에 영향을 주지 않으면서 현재 사용자 계정에서만 다른 버전의 JDK를 사용하고자 한다면 위와 같이 직접 다운로드하는 방법을 추천한다. 앞서 생성한 tomcat 사용자로 로그인하여 다운로드 및 압축 해제 후 JDK에 대한 심볼릭 링크를 생성한다.

Apache Tomcat 다운로드 및 설치하기

$ wget http://apache.tt.co.kr/tomcat/tomcat-8/v8.0.15/bin/apache-tomcat-8.0.15.tar.gz
$ tar -zxvf apache-tomcat-8.0.15.tar.gz
$ rm apache-tomcat-8.0.15.tar.gz
$ ln -s apache-tomcat-8.0.15 tomcat
  • Apache Tomcat을 다운로드한다. 여기서는 공식 홈페이지 상의 8.0.15 버전의 tar.gz 파일 링크를 그대로 다운로드 후 압축을 해제한다.
  • 추후 관리의 편의를 위해 tomcat이란 이름으로 압축 해제된 디렉토리를 바라보는 심볼릭 링크를 생성한다.

환경변수 및 인코딩 설정하기

$ vi $HOME/.bashrc
export PATH="$HOME/jdk/bin:$HOME/tomcat/bin:$PATH"
export JAVA_HOME="$HOME/jdk"
export JRE_HOME="$HOME/jdk/jre"
export CATALINA_HOME="$HOME/tomcat"
export CATALINA_OPTS="-server -Xms256m -Xmx256m"

$ vi $CATALINA_HOME/conf/server.xml
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8"/>
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" URIEncoding="UTF-8"/>
  • 설치한 Apache Tomcat 8JDK 7을 기반으로 실행한다는 것을 명시하기 위해 tomcat 사용자의 홈 디렉토리의 .bashrc 파일을 열고 가장 마지막에 위와 같이 환경변수를 추가한다.
  • CATALINA_OPTS-Xms, -Xmx 옵션은 Tomcat에서 가용할 수 있는 Heap Size의 최소, 최대 크기를 의미한다. 시스템의 쓰임새와 메모리 용량을 고려하여 적절한 값을 설정한다. Heap Size가 초과되면 java.lang.OutOfMemoryError: Java heap space 오류가 발생한다.
  • URIEncoding 옵션을 별도로 지정하지 않으면 Apache Tomcat은 기본 캐릭터셋으로 서블릿 스펙에 명시된 ISO-8859-1을 사용한다. ISO-8859-1은 대부분의 서유럽 언어만을 수용하기 때문에 요청 URI에 한글이 포함되면 깨져버린다. URIEncodingUTF-8로 지정해야 HTTP Request시 한글 파라메터가 깨지지 않고 온전히 서버로 전달된다.

Apache Tomcat Manager 접근 권한 부여하기

$ vi $CATALINA_HOME/conf/tomcat-users.xml
<tomcat-users>
    <role rolename="manager-gui"/>
    <user username="$USERNAME" password="$PASSWORD" roles="manager-gui"/>
</tomcat-users>

$ vi $CATALINA_HOME/webapps/manager/WEB-INF/web.xml
    <multipart-config>
      <max-file-size>104857600</max-file-size>
      <max-request-size>104857600</max-request-size>
      <file-size-threshold>0</file-size-threshold>
    </multipart-config>
  • Apache Tomcat Manager는 웹으로 애플리케이션을 배포하고 관리할 수 있는 도구이다. 접근 권한을 가진 사용자 계정을 생성하면 http://localhost:8080/manager/html 주소를 실행하여 원격 관리가 가능하다.
  • Apache Tomcat Manager 또한 Apache Tomcat에 의해 실행되는 Java Web Application이다. 배포를 위한 .war 업로드시 최대 파일 크기가 50MB로 제한되어 있는데 크기를 늘리려면 위와 같이 web.xmlmax-file-size, max-request-size 엘러먼트의 값을 수정하면 된다. 값의 단위는 byte이다.

Apache Tomcat 서비스 시작하기

$ $CATALINA_HOME/bin/startup.sh
  • Apache Tomcat 서비스를 시작한다. http://localhost:8080/ 주소로 접속하여 서비스가 정상적으로 실행되고 있는지 확인한다.

참고 글


저작자 표시 비영리 동일 조건 변경 허락
신고

'소프트웨어개발 > Tomcat' 카테고리의 다른 글

CentOS에서 Apache Tomcat 설치하기  (0) 2014.12.01

Apache Tomcat, java.lang.OutOfMemoryError: Java heap space 오류 발생시 대처 방법

java.lang.OutOfMemoryError: Java heap space

Apache Tomcat에서 실행 중인 웹 애플리케이션에서 java.lang.OutOfMemoryError: Java heap space 오류가 발생하는 경우 어떻게 조치해야 할까? 절차를 아래와 같이 정리해봤다.

  • 먼저 Heap이란 용어를 이해해야 한다. Heap이란 Java Application이 실행 중에 생성되는 Object가 저장되는 메모리 영역이다.(당연히 Garbage Collection이 발생하는 영역이기도 하다.) Heap Size는 바로 이러한 Heap의 크기를 의미하며 당연히 Heap Size가 클수록 좋다. Heap Size가 작으면 java.lang.OutOfMemoryError: Java heap space 오류가 발생할 확률이 높아진다. 물론 Heap 사용량을 유발하는 코드 상의 구조적인 결함이 있는지도 점검해봐야 한다. 구조적인 결함을 놔둔채 Heap Size만 늘리면 애플리케이션의 시한부 생명을 조금이나마 더 늘리는 것에 불과하다.
  • Heap Size를 늘리기 위해 실행 중인 Apache Tomcat 서비스를 중지하고 운영체제 별로 아래 명령을 실행하고 서비스를 재시작한다.
// Linux 기반의 Bash 쉘일 경우
export CATALINA_OPTS="-Xms1024m -Xmx1024m"

// Windows 기반일 경우
set CATALINA_OPTS=-Xms1024m -XmX1024m
  • 위의 옵션이 바로 Heap Size를 결정하는 옵션으로 -Xms는 최소 Heap Size, -Xmx는 최대 Heap Size를 의미한다. 1024m은 1024MB를 의미한다. 1g를 입력해도 의미는 동일하다. 시스템의 메모리는 물리적으로 한정된 공간이므로 Heap Size를 무조건 높게 잡을 수는 없다. 최대값은 사용 가능한 시스템 메모리의 75% 정도를 할당하자. 최소값과 최대값 모두 같은 값을 할당하는 것도 좋은 방법이다.

참고 글

저작자 표시 비영리 동일 조건 변경 허락
신고

서버-클라이언트(Java-JavaScript)간 날짜/시간 데이터 다루기

개요

서버-클라이언트의 개발 언어가 다를 경우(대부분의 개발 환경에 해당한다.) 날짜/시간 데이터를 어떻게 전달하고 처리해야 할까? 서버와 클라이언트는 개발 언어도 다르고 날짜와 시간을 다루는 타입과 방법도 다르다. 이러한 고민을 하는 과정에서 내가 했던 방법을 정리하여 소개하고자 한다. 서버의 개발 언어는 Java, 클라이언트는 JavaScript를 기준으로 설명한다.

서버의 날짜와 시간은 대부분 데이터베이스에서 가져온다.

서버 환경에서는 날짜/시간에 대한 기준이 필요하다. 그 기준은 서버 애플리케이션이 실행되는 운영체제의 시간이 될 수도 있고 RDBMS의 시간이 될 수도 있으며 별도의 타임 서버가 기준이 될 수도 있다. 일반적으로 애플리케이션의 성격에 맞게 결정되지만 대부분의 애플리케이션의 기능이 데이터베이스에 대한 CRUD가 차지하므로 내 경우 DB를 기준으로 시간을 정한다. Java에서는 JDBC가 제공하는 API를 사용하여 데이터베이스에 접근하는데 10억분의 1초까지 저장할 수 있는 java.sql.Timestamp 클래스를 사용하여 날짜/시간 데이터를 다룬다.

// Sql2o와 같은 JDBC 랩퍼 라이브러리를 사용하여 Oracle 데이터베이스에 접근할 경우 클래스 타입을 별도로 명시하지 않으면 날짜/시간 데이터가 java.sql.Timestamp가 아닌 oracle.sql.TIMESTAMP 클래스의 오브젝트에 저장된다. 이 경우 timestampValue() 메써드를 사용하여 java.sql.Timestamp 오브젝트로 변환한다. 내 경우 Timestamp 오브젝트를 그대로 JSON으로 변환하여 클라이언트에 전달한다.
Timestamp someTimestamp = oracleTimeStamp.timestampValue();

// TimeStamp 오브젝트를 그대로 사용해도 상관없지만 서버 차원에서 날짜/시간을 가공할 필요가 있을 경우 JodaTime 라이브러리를 사용한다. org.joda.time.DateTime 클래스의 생성자로 Timestamp 오브젝트를 전달하면 DateTime 오브젝트를 생성할 수 있다. DateTime 오브젝트는 JSON으로 변환시 Timestamp 값으로 변환된다.
DateTime dateTime = new DateTime(someTimestamp);

클라이언트에서 서버로부터 전달받은 날짜/시간 다루기

Java(서버)와 JavaScript(클라이언트)는 기본 데이터 타입이 완벽하게 일치하지 않기 때문에 내 경우 서버에서 클라이언트로 전달할 데이터를 생성할 경우 모든 데이터를 문자열로 처리하여 JSON 객체에 담아 전달한다. 시간/문자열 데이터는 앞서와 같이 Timestamp 또는 DateTime 오브젝트를 변환했을 경우 Timestamp 형식의 값이 문자열로 전달된다. 클라이언트에서 JSON 데이터를 파씽하여 someTimestampString이란 이름의 변수에 파씽한 날짜/시간 데이터를 저장했다고 가정하고 아래 예제를 진행해보자.

// 파씽된 데이터는 문자열 변수로 인식된 상태로 아래와 같이 숫자로 변경해야 한다.
var someTimestamp = Number(someTimestampString);

// JavaScript는 날짜/시간을 다루기 위한 Date 객체를 제공하며 Timestamp 값을 받아 새로운 객체를 생성할 수 있다.
var dateTime = new Date(someTimestamp);

// 내 경우 날짜/시간 관련 다양한 기능을 제공하는 Sugar.js 라이브러리를 사용한다. 아래와 같이 사용한다.
var dateTime = Date.create(someTimestamp);

앞의 과정을 통해 서버로 전달받은 날짜/시간 데이터를 JavaScriptDate 객체로 인식하는 것까지 완료했다. 다음은 사용자에게 이 값을 어떤 형식으로 출력할 것인가의 결정이 남았다. 국가, 언어, 문화에 따라 날짜/시간은 다양한 형식으로 출력될 수 있다. Sugar.js의 힘을 빌어 아래와 같이 간단하게 출력 형식을 결정할 수 있다.

// 국제표준 출력 형식인 ISO8601_DATE 형식으로 출력할 수 있다. 날짜만 출력할 경우 가장 많이 사용되는 형식이다.
var dateFormatted = dateTime.format('{yyyy}-{MM}-{dd}'); // 2014-01-01

// 국제표준 출력 형식인 ISO8601_DATETIME 형식으로 출력할 수 있다. 날짜와 시간 사이에 T 문자가 삽입된다. 시간이 UTC일 경우 Z 문자가 끝에 삽입되며 UTC가 아닐 경우 UTC 기준 추가 시간을 + 기호로 추가 출력한다. 따라서 한국의 경우 +09:00가 출력된다.
var dateTimeFormatted = dateTime.format(Date.ISO8601_DATETIME); // 2014-01-01T00:00:01+09:00

// 시간까지 출력할 경우 국제표준을 따르는 것이 깔끔한 방법이기는 하나 국내 사용자들 입장에서는 가독성 문제가 제기될 수 있다. 내 경우 아래와 같이 24시간제를 적용하여 시분초까지 출력한다.
var dateTimeFormatted = dateTime.format('{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}'); // 2014-01-01 00:00:01

참고 글


저작자 표시 비영리 동일 조건 변경 허락
신고