스프링부트 (8)

💻 Programming/Java

[5분코딩] Spring Boot Cache with EHCACHE

안녕하세요~ 오랜만에 포스팅을 하게되네요.

오늘은 스프링부트 기반의 프로젝트에서 스프링캐시를 사용하는 방법에 대해서 공유드리려고 합니다.

스프링캐시는 레디스, 카페인, ehcache 등과 연계하여 사용하게 되는데요,

이번  포스팅에서는 ehcache와 연동하여 사용할 예정입니다.

우선 기본적인 개발환경은 아래와 같습니다.

  • IntelliJ IDEA 2021.3.3 (Ultimate Edition)
  • SpringBoot 2.7.3
  • Java 18 (OpenJDK)

1. 프로젝트 생성

우선 빈 깡통 프로젝트를 만들어 볼게요.

이미 사용할 프로젝트가 있다면 건너뛰셔도 좋습니다 :)

자바 18, 그래들 선택
아무런 의존성을 선택하지 않고 깨끗한 프로젝트로 만듭니다
프로젝트 빌드가 완료된 후 프로젝트 트리

자, 빈깡통 프로젝트 생성이 완료되었습니다.

2. 라이브러리 추가 

이제 구현 및 테스트에 필요한 라이브러리들을 추가해볼게요.

우선 현재 build.gradle 파일은 아래와 같이 되어있을거에요

plugins {
    id 'org.springframework.boot' version '2.7.3'
    id 'io.spring.dependency-management' version '1.0.13.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '18'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

저기에 아래 5개 라이브러리들을 추가해줍니다.

implementation 'org.ehcache:ehcache:3.10.1'
implementation 'org.springframework:spring-context-support:5.3.22'

implementation 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'

implementation 'ch.qos.logback:logback-classic:1.2.11'

왜 저 라이브러리들이 필요한지 하나씩 볼게요.

implementation 'org.ehcache:ehcache:3.10.1' jcache 구현체인 ehcache 사용을 위함
implementation 'org.springframework:spring-context-support:5.3.22' 스프링캐시의 어노테이션을 사용하기 위함
implementation 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'
lombok 어노테이션을 사용하기 위함
implementation 'ch.qos.logback:logback-classic:1.2.11' 로깅을 위함

이렇게 라이브러리를 추가한 뒤 캐시설정을 좀 해주도록 할게요

3. 캐시 설정

com.example.demo.config 패키지를 만든 뒤 아래와 같이 CacheConfig 클래스를 작성해줍니다.

package com.example.demo.config;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.jcache.JCacheCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.cache.Cache;
import javax.cache.Caching;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.expiry.CreatedExpiryPolicy;
import javax.cache.expiry.Duration;
import javax.cache.spi.CachingProvider;
import java.util.concurrent.TimeUnit;

@EnableCaching
@Configuration
public class CacheConfig {

    private final CachingProvider cachingProvider = Caching.getCachingProvider();
    private final javax.cache.CacheManager cacheManager = cachingProvider.getCacheManager();

    @Bean
    public CacheManager cacheManager() {
        return new JCacheCacheManager(cacheManager);
    }

    @Bean
    public Cache<Integer, Integer> commonCache() {
        MutableConfiguration<Integer, Integer> configuration =
                new MutableConfiguration<Integer, Integer>()
                        .setTypes(Integer.class, Integer.class)
                        .setStoreByValue(false)
                        .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(new Duration(TimeUnit.SECONDS, 5)));
        return cacheManager.createCache("commonCache", configuration);
    }
}

위 내용을 간략히 살펴보면 cacheManager 빈을 하나 등록해주고 그 캐시매니저에 commonCache라는 이름의 캐시 빈을 하나 등록해주는 겁니다. 이 commonCache가 key, value 페어를 저장하는 하나의 서랍이라고 생각하시면되고요. 여기서는 키와 값의 타입이 모두 Integer 이고 만료정책은 생성시간을 기준으로 5초로 설정해주었습니다.

 

4. API 구현 및 테스트

이제 commonCache에 값을 저장하고 만료되기 전까지 캐시의 데이터를 사용하도록 서비스 클래스를 하나 만들어주겠습니다.

package com.example.demo.service;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class CacheService {

    @Cacheable(cacheNames = "commonCache", cacheManager = "cacheManager", key = "#code")
    public Integer getCachedValue(int code) {
        System.out.println("계산중....");
        // DB 조회 등의 로직...
        return (int) (Math.random() * 10);
    }
}

com.example.demo.service 패키지를 만들고 CacheService 클래스를 작성했습니다.

getCachedValue 메서드에 @Cacheable 어노테이션을 붙여주고 commonCache를 사용하고 cacheManager는 CacheConfig에 등록했던 cacheManager 이름을 적어주었습니다. 그리고 key로 파라미터로 전달받는 code 값을 사용하겠다고 선언해주었습니다. 만약 key로 어떤 객체의 멤버변수를 사용해야한다면 key = "#obj.employeeId" 와 같이 사용가능합니다. (공식문서 참고)

CacheService.getCachedValue 가 호출되면 code 값을 key로하여 return된 Integer 값을 commonCache에 저장하게되고 만료시간인 5초 이내에 동일한 key값(여기서는 code 파라미터 값)으로 메서드 호출이 발생할 경우 메서드 내부의 비즈니스 로직을 타지않고 바로 캐시에 저장되어있는 값을 반환합니다. 이를 확인하기위해 계산중.... 메시지를 출력하도록 했습니다.

 

마지막으로 위 서비스를 호출하는 테스트 API 를 만들어 줄게요.

com.example.demo.controller 패키지를 신규추가하고 TestController를 생성하겠습니다.

package com.example.demo.controller;

import com.example.demo.service.CacheService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/test")
@RequiredArgsConstructor
public class TestController {

    private final CacheService cacheService;

    @GetMapping("/cache")
    public Integer getCachedValue(int code) {
        log.info("Request accepted for code {}", code);
        return cacheService.getCachedValue(code);
    }
}

API 호출이 들어올때마다 Request accepted for code X 로그가 출력되도록 하고, 캐시서비스의 메서드를 호출하도록 했습니다.

이제 DemoApplication을 실행하고 API를 호출해볼게요.

2022-09-14 12:32:59.783  INFO 47720 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 0.778 seconds (JVM running for 0.997)
2022-09-14 12:33:06.871  INFO 47720 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-09-14 12:33:06.871  INFO 47720 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2022-09-14 12:33:06.872  INFO 47720 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2022-09-14 12:33:06.882  INFO 47720 --- [nio-8080-exec-1] c.e.demo.controller.TestController       : Request accepted for code 1
계산중....
2022-09-14 12:33:08.270  INFO 47720 --- [nio-8080-exec-2] c.e.demo.controller.TestController       : Request accepted for code 1
2022-09-14 12:33:09.186  INFO 47720 --- [nio-8080-exec-3] c.e.demo.controller.TestController       : Request accepted for code 1
2022-09-14 12:33:10.017  INFO 47720 --- [nio-8080-exec-4] c.e.demo.controller.TestController       : Request accepted for code 1
2022-09-14 12:33:10.880  INFO 47720 --- [nio-8080-exec-5] c.e.demo.controller.TestController       : Request accepted for code 1
2022-09-14 12:33:11.764  INFO 47720 --- [nio-8080-exec-6] c.e.demo.controller.TestController       : Request accepted for code 1
2022-09-14 12:33:12.631  INFO 47720 --- [nio-8080-exec-7] c.e.demo.controller.TestController       : Request accepted for code 1
계산중....
2022-09-14 12:33:13.538  INFO 47720 --- [nio-8080-exec-8] c.e.demo.controller.TestController       : Request accepted for code 1
2022-09-14 12:33:14.408  INFO 47720 --- [nio-8080-exec-9] c.e.demo.controller.TestController       : Request accepted for code 1

호출은 모두 code값 1을 넣어서 하였고, 최초 호출을 12:33:06.882 에 하면서 서비스 내부로직을 실행하여 값을 반환하였습니다. (계산중.... 출력으로 확인)

이후 12:33:11.764 까지는 계산중.... 이 출력되지 않은 것으로 보아 캐싱되어있는 값을 그대로 응답한걸로 볼 수 있습니다.

5초가 지난 12:33:12.631 에 들어온 호출건은 계산중.... 이 출력된걸로 보아 응답값에 변동이 발생했을 것으로 추측됩니다. (랜덤값으로 응답하는 서비스니 응답값에 변동이 없을 수도 있죠)

여기서 중요한 것은 응답값에 변동이 있느냐가 아니라 캐시되어있는 데이터가 만료되기 전까지 메서드 내부로직을 실행하지 않고 캐시에서 조회된 데이터를 리턴해준다는 점입니다.

 

이상으로 5분만에 스프링캐시와 ehcache를 이용하여 캐시를 구현하는 방법에 대해 알아보았습니다.

 

읽어주셔서 감사합니다~  🙇

💻 Programming/Java

SpringBoot에서 ThreadPoolTaskExecutor 설정하기

이번 포스팅에서는 SpringBoot에서 ThreadPoolTaskExecutor를 어떻게 간단하게 설정하여 사용할 수 있는지 알려드립니다.

SpringBoot 프로젝트는 이미 구성되어 있다는 전제하에 Java11, SpringBoot 2.4.2 기준으로 작성했습니다.

참고로 ThreadPoolTaskExecutor의 설정 및 구체적인 사용법에 대해서는 이전에 포스팅한 문서를 참고하시기 바랍니다.

 

ThreadPoolTaskExecutor의 사용법

 

ThreadPoolTaskExecutor를 이용하여 비동기 처리하기(멀티쓰레드)

async(비동기) 처리를 위한 ThreadPoolTaskExecutor ThreadPoolTaskExecutor를 이용하여 비동기처리하는 방법을 알아보겠습니다. ThreadPoolTaskExecutor는 스프링에서 제공해주는 클래스로 org.springframework.s..

keichee.tistory.com

ThreadPoolTaskExecutor Bean 등록하기

우선 ThreadPoolTaskExecutor를 빈으로 등록하도록 하겠습니다. 

TaskExecutorConfig 클래스를 만들고 @Configuration 어노테이션을 추가하여 설정을 위한 클래스를 하나 만들었습니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
public class TaskExecutorConfig {

	@Bean(name = "executor")
	public ThreadPoolTaskExecutor executor() {
		ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
		executor.setThreadNamePrefix("my-");
		return executor;
	}
}

그리고 executor() 메서드를 하나 만들어서 ThreadPoolTaskExecutor를 생성하여 반환해주도록 하고 @Bean으로 등록하면서 name 속성을 지정해주었습니다. 참고로 name 속성의 값은 메서드명과 달라고 무관합니다. 또한, 해당 executor를 이용하여 쓰레드를 실행시켰을 때 내가 만든 ThreadPoolTaskExecutor가 사용되는 것인지 확인할 수 있도록 쓰레드명prefix를 "my-"로 세팅해주었습니다. 

이전 포스팅을 읽어보셨거나 또는 ThreadPoolTaskExecutor를 직업 new 키워드로 생성해서 사용해보신 분은 눈치채셨을 수도 있을텐데요, 여기서 initialize()를 하지 않았습니다. 공식문서에는 initialize 메서드를 호출하는 것으로 나와있지만 굳이 하지 않아도 빈으로 등록될 때 initialize를 하더군요.

 

@Autowired 로 ThreadPoolTaskExecutor 사용하기

아래는 빈으로 등록한 executor를 서비스 레이어에서 autowired하여 사용하는 예제입니다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class TestService {

  @Autowired
  @Qualifier("executor")
  private ThreadPoolTaskExecutor executor;

  public void executeThreads() {
      log.info("executing threads....");
      Runnable r = () -> {
          try {
              log.info(Thread.currentThread().getName() + ", Now sleeping 10 seconds...");
              Thread.sleep(10000);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      };

      for (int i = 0; i < 10; i++) {
          executor.execute(r);
      }
  }
}

TestService라는 서비스 클래스에서 멤버변수 executor에 위에서 빈으로 등록한 객체를 쓰도록 @Qualifier로 executor 이름을 명시해주었습니다.

위 코드를 실행해보면 쓰레드 명이 my-1 로 출력되는 것을 확인하실 수 있습니다. corePoolSize를 default 값인 1 로 사용하여 싱글 쓰레드로 실행되기 때문에 my-2, my-3은 보이지 않습니다. corePoolSize 를 2 이상으로 세팅해주면 my-2, my-3 등의 쓰레드도 확인가능합니다.

2021-01-26 18:16:23.147  INFO 16390 --- [           my-1] com.keichee.test.service.TestService     : my-1, Now sleeping 10 seconds...
2021-01-26 18:16:33.150  INFO 16390 --- [           my-1] com.keichee.test.service.TestService     : my-1, Now sleeping 10 seconds...
2021-01-26 18:16:43.152  INFO 16390 --- [           my-1] com.keichee.test.service.TestService     : my-1, Now sleeping 10 seconds...
2021-01-26 18:16:53.157  INFO 16390 --- [           my-1] com.keichee.test.service.TestService     : my-1, Now sleeping 10 seconds...

 

@Async로 ThreadPoolTaskExecutor 사용하기

@Async 어노테이션을 이용하여 위에서 빈으로 등록한 ThreadPoolTaskExecutor를 사용하려면 @EnableAsync 도 추가해주어야 합니다.

따라서 TaskExecutorConfig 클래스에 @EnableAsync 어노테이션을 추가해줍니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@EnableAsync
@Configuration
public class TaskExecutorConfig {

	@Bean(name = "executor")
	public ThreadPoolTaskExecutor executor() {
		ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
		executor.setThreadNamePrefix("my-");
		return executor;
	}
}

 

그리고 서비스 레이어의 메서드를 하나 추가하겠습니다.

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

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class TestService {

    @Async("executor")
    public void task() {
        try {
            log.info(Thread.currentThread().getName() + ", Now sleeping 10 seconds...");
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

이제 controller 쪽에서 task() 메서드를 loop 돌면서 호출해보면 @Autowired 를 이용한 방법과 동일하게 로그가 출력되는 것을 확인하실 수 있을 겁니다.

 

이상으로 SpringBoot에서 ThreadPoolTaskExecutor 설정하여 사용하는 방법에 대해서 알아보았습니다.

간략하게 다시 정리해보자면

 

1. @Configuration 으로 등록한 클래스에 executor @Bean 추가 (@Async 를 이용할 경우 @EnableAsync 도 추가)

2. @Autowired @Qualifier 로 주입하여 사용하거나 또는 메서드 레벨에 @Async 를 붙여 사용

 

이렇게 정리할 수 있겠네요.

 

그럼 오늘도 행복한 코딩하세요~

안녕하세요, 케이치입니다.

오늘은 연락처 관리 프로그램을 만드는 웹앱 프로젝트를 진행해보려고 합니다.

사용할 도구는 Eclipse 2020-06 R 이고요, 스프링부트부트스트랩, 그리고 데이터베이스 연동은 mampmybatis를 사용합니다.

우선 기본적인 도구들과 라이브러리들 그리고 플러그인들을 설치해야합니다.

개발환경은 다음과 같습니다.

 

  1. MacOS Catalina (10.15.6)
  2. Eclipse 2020-06 R (Download)
  3. Eclipse Spring Tool Suite 4 (설치하기 가이드)
  4. MAMP (Download) -> community 버전 다운로드 받으세요, Window사용자의 경우 WAMP를 설치하셔야 합니다.
  5. DBeaver (데이터베이스 무료 툴 Download) -> community 버전 다운로드 받으세요

 

자, 위 도구들을 모두 설치완료했다면 이제부터 시작해보겠습니다.

 

우선 결과물은 아래 동영상처럼 나올겁니다.

 

연락처 관리 웹앱

자, 그럼 어떻게 만드는지 본격적으로 알아보도록 하겠습니다.

진행은 아래 순서대로 합니다.

  1. 테이블 설계
  2. 백엔드 구현
  3. 프론트 구현

 

테이블 설계

우선 테이블 설계부터 하겠습니다. (MAMP설치 및 테이블 소유자 권한 등의 설정은 여기서 다루지 않습니다.)

테이블은 contact 라고 명명지어주고 컬럼은 아래처럼 두 개만 추가를 해줄 겁니다.

CREATE TABLE `contact` (
  `name` varchar(100) NOT NULL,
  `phone` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

 

 

DBeaver에서 조회한 contact 테이블 명세

정말 간단한 연락처 관리 프로그램이므로 이름과 연락처 정보만 담을 예정이고 개인정보 암호화 같은건 다루지 않습니다.

입력받은 정보 그대로 string으로 저장을 하도록 하겠습니다.

이렇게 테이블 설계가 완료되면 이제 백엔드에서 쿼리해올 수 있도록 구현을 들어가보겠습니다.

 

백엔드 개발

우선 새로운 프로젝트를 만들어야겠죠.

아래 순서대로 새로운 프로젝트를 만들어 줍니다.

Project Explorer > New > Project 선택

 

Wizards에서 spring으로 검색한 뒤 Spring Starter Prject 선택 (이게 안나온다면 STS설치하기부터 하고 오시면 됩니다.)

 

 

Name 항목은 프로젝트명을 입력하는 곳입니다.

여기에 contact-manager 라고 넣어주시고 Java Version은 8 이상으로 아무거나 쓰셔도 무방할겁니다.

여기서는 Java 11 버전을 사용하였습니다.

 

Next를 눌러 다음으로 넘어간 뒤 Available 검색창에서 spring web, lombok, mybatis, mysql를 검색하여 우측의 selected 항목에 세 개 항목이 들어가도록 체크하고 Finish 버튼을 클릭합니다. 저는 이미 사용한 적이 있어서 Frequently Used에 항목이 나타나서 체크만 해주었습니다. 처음 하시는 분들은 Available에서 검색하시면 됩니다.

 

우측 하단에 progress bar가 진행되는 것을 확인할 수 있습니다.

 

또는 Progress View에서 확인할 수도 있죠. Progress View가 안보인다면 이클립스 상단 메뉴에서 Window > Show View > Other > Progress로 검색하면 추가할 수 있습니다.

 

 

진행이 완료되었으면 Project Explorer에서 아래처럼 파일트리가 보일겁니다.

pom.xml 파일에는 아래와 같은 내용이 들어있죠.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.2.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>contact-manager</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>contact-manager</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>11</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.1.3</version>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

 

 

이제 자바로 코딩할 준비가 되었습니다.

 

우선 위에서 만든 contact 테이블의 내용을 조회해오는 API를 만들어 보도록 하겠습니다.

 

com.example.demo 패키지 하위에 domain 라는 이름의 패키지를 만들고 해당 패키지에 ContactInfo.java 파일을 생성해줍니다.

그리고 아래와 같은 내용을 넣어줍니다.

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class ContactInfo {
    private String name;
    private String phone;
}

 

ContactInfo 클래스는 contact 테이블과 연동할 dto입니다.

 

com.example.demo 패키지 하위에 mapper 라는 이름의 패키지를 만들고 해당 패키지에 ContactInfoMapper.java 파일을 생성해줍니다. ContactInfoMapper는 클래스가 아닌 인터페이스로 생성해주고 아래와 같이 쿼리문을 작성해줍니다.

import com.example.demo.domain.ContactInfo;
import org.apache.ibatis.annotations.*;

import java.util.List;

@Mapper
public interface ContactInfoMapper {

    @Select("select * from contact")
    List<ContactInfo> selectAll();

    @Insert("insert into contact (name, phone) values (#{name}, #{phone})")
    int insert(ContactInfo contactInfo);

    @Delete("delete from contact where name = #{name}")
    int delete(String name);

    @Select("<script>" +
            "select * from contact " +
            "<where>" +
            "<if test=\"name != null and name.length > 0\">and name = #{name}</if>" +
            "<if test=\"phone != null and phone.length > 0\">and phone = #{phone}</if>" +
            "</where>" +
            "</script>")
    List<ContactInfo> selectBy(@Param("name") String name, @Param("phone") String phone);

    @Update("update contact " +
            "set phone = #{phone} " +
            "where name = #{name}")
    int update(String name, String phone);
}

 

이 ContactInfoMapper 인터페이스는 마이바티스와 연동이 되어 쿼리를 database로 전달하고 결과를 받아오는 역할을 합니다.

 

 

com.example.demo 패키지 하위에 service 라는 이름의 패키지를 만들고 해당 패키지에 ContactInfoService.java 파일을 생성해줍니다.

이 파일에는 아래와 같이 내용을 넣어주시면 됩니다.

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.demo.domain.ContactInfo;
import com.example.demo.mapper.ContactInfoMapper;

@Service
public class ContactInfoService {

	@Autowired
	private ContactInfoMapper contactInfoMapper;

	public List<ContactInfo> getEveryContactInfo() {
		return contactInfoMapper.selectAll();
	}

	public int addContactInfo(ContactInfo contactInfo) {
		return contactInfoMapper.insert(contactInfo);
	}

	public int delContactInfo(String name) {
		return contactInfoMapper.delete(name);
	}

	public List<ContactInfo> getContactInfos(String name, String phone) {
		return contactInfoMapper.selectBy(name, phone);
	}

	public int updateContactInfo(String name, String phone) {
		return contactInfoMapper.update(name, phone);
	}
}

 

ContactInfoMapper의 메서드를 호출하는 역할만 하고 있지만 프로젝트가 복잡해지면 각종 비즈니스 로직이 들어가야할 클래스입니다.

조회한 뒤에 정렬을 한다든지, 필터링을 한다든지 하는 로직은 이 서비스 클래스에 넣어주시면 됩니다.

 

이제 controller쪽에서 사용할 응답 도메인 클래스를 하나 더 만들어 보겠습니다.

com.example.demo.domain 패키지에 Response.java 파일을 생성하고 아래와 같이 작성해줍니다.

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@JsonInclude(Include.NON_NULL)
public class Response<T> {

    private int code;
    private String message;
    private T data;

    public Response(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public Response(int code, String message, T data) {
        this(code, message);
        this.data = data;
    }
}

 

마지막으로 com.example.demo 패키지 하위에 controller 라는 이름의 패키지를 만들고 해당 패키지에 ContactInfoController.java 파일을 생성해서 아래 내용을 넣어주세요.

import com.example.demo.domain.ContactInfo;
import com.example.demo.domain.Response;
import com.example.demo.service.ContactInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Slf4j
@RestController
@RequestMapping("/contact-info")
public class ContactInfoController {

	@Autowired
	private ContactInfoService contactInfoService;

	@GetMapping("/list") // --> localhost:8080/contact-info/list
	public List<ContactInfo> getAllContactInfo() {
		return contactInfoService.getEveryContactInfo();
	}

	@GetMapping("/get")
	public ResponseEntity<Response> getContactInfos(
			@RequestParam(required = false) String name,
			@RequestParam(required = false) String phone) {
		List<ContactInfo> result = contactInfoService.getContactInfos(name, phone);
		if (result.size() == 0) {
			return ResponseEntity.status(HttpStatus.OK).body(new Response(204, "데이터 없음"));
		}
		return ResponseEntity.ok(new Response(200, "조회 성공", result));
	}

	@PostMapping("/add")
	public ResponseEntity<Response> addNewContactInfo(@RequestBody ContactInfo contactInfo) {
		contactInfoService.addContactInfo(contactInfo);
		return ResponseEntity.ok(new Response(200, "등록 성공"));
	}

	@DeleteMapping("/del")
	public ResponseEntity<Response> delContactInfo(@RequestParam String name) {
		int result = contactInfoService.delContactInfo(name);
		if (result == 0) {
			return ResponseEntity.status(HttpStatus.OK).body(new Response(204, "데이터 없음"));
		}
		return ResponseEntity.ok(new Response(200, "삭제 성공"));
	}

	@PutMapping("/update")
	public ResponseEntity<Response> updateContactInfo(@RequestParam String name, @RequestParam String phone) {
		int result = contactInfoService.updateContactInfo(name, phone);
		if (result == 0) {
			return ResponseEntity.status(HttpStatus.OK).body(new Response(204, "데이터 없음"));
		}
		return ResponseEntity.ok(new Response(200, "수정 성공"));
	}

	@ExceptionHandler
	public ResponseEntity<Response> contactInfoErrorHandler(Exception e) {

		log.error("error!!, {}", e.getClass().getName(), e);

		if (e instanceof DuplicateKeyException) {
			return ResponseEntity.status(HttpStatus.CONFLICT).body(new Response(409, "이미 중복된 이름이 등록되어있습니다."));
		} else {
			return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new Response(500, "서버오류가 발생하였습니다."));
		}
	}
}

이 클래스가 프론트와 백엔드를 연결해주는 부분으로 API url을 정의하고 어떤 파라미터들을 전달받아야 하는지, 응답은 어떻게 주고 예외처리는 어떻게 하는지에 대한 내용이 담겨있죠. 

 

이제 백엔드 구현은 완료되었습니다. 아 참!! 하나 빼먹은게 있네요. database 접속 정보를 빼먹었습니다. 자, src/main/resources 하위의 application.properties 파일을 열어서 직접 생성하셨던 database 접속 정보를 아래처럼 넣어주세요.

spring.datasource.url=jdbc:mysql://localhost:8889/javastudy?useSSL=false&serverTimezone=UTC
spring.datasource.username=javastudy
spring.datasource.password=javastudy
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

접속 포트(8889)와 database명(javastudy) 그리고 username, password 정보를 생성하신 데이터베이스에 맞게 변경해주셔야 합니다.

 

여기까지 완료되었다면 Project Explorer에 아래와 같이 파일들이 존재해야 합니다.

이제 ContactManagerApplication을 Sppring Boot App으로 기동해서 API가 잘 동작하는지 확인을 해보도록 하겠습니다.

앱 기동에 성공하면 Console View에서 아래 로그를 확인 할 수 있습니다.

INFO 69998 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
INFO 69998 --- [main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
INFO 69998 --- [main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.37]
INFO 69998 --- [main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
INFO 69998 --- [main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1185 ms
INFO 69998 --- [main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
INFO 69998 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
INFO 69998 --- [main] c.e.demo.ContactManagerApplication       : Started ContactManagerApplication in 2.457 seconds (JVM running for 3.521)

 

이제 브라우저 창을 열고 http://localhost:8080/contact-info/get 을 주소창에 입력하고 호출해보겠습니다.

아래처럼 출력이 되면 정상적으로 잘 동작한 겁니다.

{"code":204,"message":"데이터 없음"}

 

여기서 혹시 에러가 난다면 위로 올라가서 다시 순서대로 따라해보세요.

그래도 안되신다면....댓글로 에러메시지를 남겨주시면 함께 고민해보도록 하겠습니다.

 

자, 이제 프론트 개발로 넘어갑니다.

 

프론트 개발

프론트 영역 개발을 위해서 pom.xml에 두 개의 dependency를 추가해주겠습니다.

<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>bootstrap</artifactId>
	<version>4.5.0</version>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

thymeleaf와 부트스트랩입니다.

 

그리고 controller 패키지 내에 ViewController.java 파일을 만들어서 아래와 같이 짧은 코드를 작성해주겠습니다.

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ViewController {

    @GetMapping("/contact-info")
    public String contactInfo() {
        return "contact-info";	// contact-info.html을 화면에 보여주는 역할
    }

}

 

그리고 src/main/resources 하위의 templates 디렉토리에 contact-info.html 파일을 하나 생성하고 아래 내용을 넣어주세요.

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">

    <!-- JS link -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script src="/webjars/bootstrap/4.5.0/js/bootstrap.min.js"></script>
    <script src="script/contact-info.js"></script>

    <!-- CSS link -->
    <link rel="stylesheet"
          href="/webjars/bootstrap/4.5.0/css/bootstrap.min.css"/>
    <link rel="stylesheet" href="style/contact-info.css"/>

</head>
<body>

<div id="content">
    <h1>연락처 관리</h1>
    <p>
        <label>이름:</label>
        <input id="name" type="text" placeholder="이름" pattern=""/>
        
        <label>연락처:</label>
        <input id="phone" type="text" placeholder="010-1234-5678"/>
        <button type="button" class="btn btn-primary btn-lg" onclick="addContact()">등록</button>
        <button type="button" class="btn btn-danger btn-lg" onclick="delContact()">삭제</button>
    </p>

    <div>
        <button type="button" class="btn btn-info btn-lg" onclick="getContacts()">연락처 목록 조회</button>
        <table>
            <thead>
	            <tr>
	                <th class="name">Name</th>
	                <th class="phone">Phone</th>
	            </tr>
            </thead>
            <tbody id="contact-info-table"></tbody>
        </table>
    </div>
</div>


<!-- Update Modal -->
<div class="modal fade" id="updateModal" tabindex="-1" role="dialog" aria-labelledby="updateModalLabel"
     aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="updateModalLabel">연락처 수정</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">

            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                <button type="button" class="btn btn-primary" onclick="updateContactInfo()">Save changes</button>
            </div>
        </div>
    </div>
</div>

</body>
</html>

 

그리고 src/main/resources 하위의 static 디렉토리 하위에 style, script 두 개의 디렉토리를 생성해줍니다.

style 디렉토리에는 contact-info.css 파일을 넣을거고, script 디렉토리에는 contact-info.js 파일을 넣을 겁니다.

각 파일의 내용은 다음과 같습니다.

 

src/main/resources/static/style/contact-info.css

body {background-color: beige;}
th {text-align:center}
.name {width:100px;}
.phone {width:200px;}
#content {margin:100px;}
#contact-info-table tr:hover {background-color: cornflowerblue;}

 

src/main/resources/static/script/contact-info.js

$( document ).ready(function() {
    getContacts();
});

function addContact() {
    let name = $('#name').val();
    if (!name) {
        alert('"name" is required!');
        return;
    }
    if (!validateName(name)) {
        alert('"name" is invalid! 한글만 입력가능합니다.');
        return;
    }
    let phone = $('#phone').val();
    if (!validatePhone(phone)) {
        alert('"phone number" is invalid!\nvalid format: 000-0000-0000');
        return;
    }

    let contactInfo = new Object();
    contactInfo.name = name;
    contactInfo.phone = phone;

    $.ajax({
        url: 'contact-info/add',
        dataType: 'json',
        type: 'post',
        contentType: 'application/json',
        data: JSON.stringify(contactInfo)
    }).done(function(response) {
        alert(response.message);
        console.log(response);
        $('#name').val('');
        $('#phone').val('');
        getContacts();
    }).fail(function(jqXHR, textStatus, errorThrown) {
        alert(jqXHR.responseText);
        console.log(jqXHR.responseText);
    });
}

function validateName(name) {
    return /^[가-힣]+$/.test(name);
// /^[A-Za-z]+$/
}
function validatePhone(phone){
    return /^\d{2,3}-\d{3,4}-\d{4}$/.test(phone);
}

function delContact() {

    let name = $('#name').val();
    if (!name) {
        alert('"name" is required!');
        return;
    }
    $.ajax({
        url: 'contact-info/del?name=' + name,
        dataType: 'json',
        type: 'delete',
        contentType: 'application/json'
    }).done(function(response) {
        alert(response.message);
        console.log(response);
        $('#name').val('');
        $('#phone').val('');
        getContacts();
    }).fail(function(jqXHR, textStatus, errorThrown) {
        alert(jqXHR.responseText);
        console.log(jqXHR.responseText);
    });
}

function getContacts() {
    let name = $('#name').val();
    let phone = $('#phone').val();

    $.ajax({
        url: 'contact-info/get' + generateQueryParams(name,phone),
        dataType: 'json',
        type: 'get',
        contentType: 'application/json'
    }).done(function(response) {
        printContactInfos(response.data);
    }).fail(function(jqXHR, textStatus, errorThrown) {
        alert(jqXHR.responseText);
        console.log(jqXHR.responseText);
    });
}

function generateQueryParams(name, phone) {
    let params = '';
    if (name || phone) {
        params = '?';
        if (name) {
            params += 'name=' + name;
        }
        if (phone) {
           if (params.length > 1) { params += '&';}
            params += 'phone=' + phone;
        }
    }
    return params;
}

function printContactInfos(contactInfos) {
    let rows = '';
    if (contactInfos) {
        contactInfos.forEach(function (item, index) {
            rows += '<tr onclick="popsUpUpdateModal(this)" ><td class="name">' + item.name
            + '</td><td class="phone">' + item.phone + '</td></tr>';
        });
    } else {
        alert('조회된 데이터가 없습니다.');
    }
    $('#contact-info-table').html(rows);
}
function popsUpUpdateModal(row) {
    let name = $(row).find('.name').text();
    let phone = $(row).find('.phone').text();
    let modalBody = '<input type="text" class="name" value="' + name + '" disabled>'
        + '<input type="text" class="phone" value="' + phone + '">';
    $('#updateModal .modal-body').html(modalBody);
    $('#updateModal').modal('show');
}
function updateContactInfo() {
    let name = $('#updateModal .modal-body .name').val();
    let phone = $('#updateModal .modal-body .phone').val();

    if (!validateName(name)) {
        alert('"name" is invalid! 한글만 입력가능합니다.');
        return;
    }
    if (!validatePhone(phone)) {
        alert('"phone number" is invalid!\nvalid format: 000-0000-0000');
        return;
    }

    $.ajax({
        url: 'contact-info/update' + generateQueryParams(name,phone),
        dataType: 'json',
        type: 'put',
        contentType: 'application/json'
    }).done(function(response) {
        alert(response.message);
        console.log(response);
        getContacts();
        $('#updateModal').modal('hide');
    }).fail(function(jqXHR, textStatus, errorThrown) {
        alert(jqXHR.responseText);
        console.log(jqXHR.responseText);
    });
}

 

각 파일들 내에서 하는 역할이 뭔지 잘 모르겠다는 분들은 댓글 달아주시면 아는 한도 내에서 답변 달아드릴게요. 

포스팅이 너무 길어져서 일일이 설명을 다 추가할 수가 없음을 양해바랍니다.

 

자, 이렇게 총 4 개의 파일을 추가하는 것만으로 프론트 개발이 완료되었습니다.

이제 다시 애플리케이션을 재기동한 뒤 화면으로 접속해서 기능들을 사용해 보겠습니다.

이번에는 localhost:8080/contact-info 주소로 접속을 하시면 됩니다. 앞에서 사용했던 /get 주소는 API를 직접 호출하는 것이니 빼고 접속해주시면 아래처럼 화면이 뜰겁니다.

연락처 관리 웹앱 완성

이 웹앱에서는 등록시에 이름을 정상적인 한글을 입력해줘야 하고 연락처의 경우 -를 포함하여 자리수를 검증하도록 되어있습니다.

검증은 contact-info.js에서 validateName(), validatePhone() 함수에서 처리하고 있으니 확인해보시기 바랍니다.

 

오류발생시에는 화면에 오류발생했다고 경고팝업창이 뜨도록 되어있으며, 서버로그를 통해 확인이 가능하도록 되어있습니다.

또한 연락처 수정은 제일 처음에 공유해드린 동영상을 따라해주시면 됩니다.

 

여기까지 30분만에 연락처 관리 웹앱 만들기 포스팅을 마치도록 하겠습니다.

 

궁금하신 사항이나 잘 안되시는 분들은 댓글 달아주시면 확인해서 답변드리도록 하겠습니다.

 

오늘도 즐프하세요~ 

 

 

Eclipse + SpringBoot + JSP 개발환경 세팅하기 2탄입니다.

오늘은 backend 보다는 frontend 작업을 위한 Bootstrap 추가를 webjar를 이용할 수 있도록 설정해보도록 할 예정입니다.

저는 backend 개발자다보니 front쪽은 자세히 알려드리기는 힘들지만 빠르게 서치해서 사용해본 경험상 webjar는 maven 의존성 관리를 통해 라이브러리를 다운받아 사용할 수 있어 매우 쉽게 라이브러리 추가가 가능합니다. org.webjars에는 bootstrap, jquery, font-awesome, swagger-ui 뿐만 아니라 angularJS, momentJS, d3js 등 매우 많은 프론트 개발을 위한 라이브러리들을 제공하고 있습니다. 매번 다운로드받아서 프로젝트에 추가할 필요 없이 백엔드 라이브러리들을 관리하듯이 메이븐 의존성으로 쉽게 관리할 수 있도록 도와주는 역할을 한다고 보시면 됩니다.

우선 지난시간에 했던 Eclipse + SpringBoot + JSP 개발환경 세팅하기 1탄 을 완성하셨던 분들을 기준으로 설명을 할 예정이니 해당 포스팅을 안보신 분들은 한번 쭈욱~ 훑어보시고 다시 오시기 바랍니다.

webjar 설정을 위해서 추가할 파일은 딱 하나입니다. org.springframework.web.servlet.config.annotation.WebMvcConfigurer를 구현하는 WebMVCConfig 클래스를 하나 생성하고 다음과 같이 addResourceHandlers 메서드를 오버라이드하여 작성해줍니다.

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
	    if (!registry.hasMappingForPattern("/webjars/**")) {
	        registry.addResourceHandler("/webjars/**").addResourceLocations(
	                "classpath:/META-INF/resources/webjars/");
	    }
	    if (!registry.hasMappingForPattern("/**")) {
	        registry.addResourceHandler("/**").addResourceLocations(
	        		new String[] {"classpath:/static/script", "classpath:/static/style"});
	    }
    }

 

우선 webjars 경로를 등록해주는 것 때문에 이 클래스를 만들어 사용하는데 이때 @EnableWebMvc 어노테이션을 사용하게 됩니다. 그런데 여기서 주의할 점이 이 어노테이션을 사용하게되면 WebMvcAutoConfiguration 이 disable되면서 기존 설정들이 정상적으로 동작하지 않게 됩니다. 따라서 기존에 잘 읽어오던 static하위의 javascript파일들과 css파일들을 못찾아 404 오류가 발생하게 되죠. 그래서 addResourceHandlers에 이 두 파일들이 존재하는 디렉토리를 classpath경로로 추가해줘야 합니다.

또한, application.properties 파일에 작성하였던 아래 두 라인은 더이상 사용하지 않고 방금 추가한 WebMVCConfig 클래스에 별도로 ViewResolver를 등록하여 사용해야합니다.

	@Bean
	public ViewResolver getViewResolver(){
	    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
	    resolver.setPrefix("/WEB-INF/views/");
	    resolver.setSuffix(".jsp");
	    resolver.setViewClass(JstlView.class);
	    return resolver;
	}

 

아래는 완성된 WebMVCConfig 클래스의 코드입니다.

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;

@Configuration
@EnableWebMvc
public class WebMVCConfig implements WebMvcConfigurer {

	@Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
		if (!registry.hasMappingForPattern("/webjars/**")) {
	        registry.addResourceHandler("/webjars/**").addResourceLocations(
	                "classpath:/META-INF/resources/webjars/");
	    }
	    if (!registry.hasMappingForPattern("/**")) {
	        registry.addResourceHandler("/**").addResourceLocations(
	        		new String[] {"classpath:/static/script", "classpath:/static/style"});
	    }
    }
	
	@Bean
	public ViewResolver getViewResolver(){
	    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
	    resolver.setPrefix("/WEB-INF/views/");
	    resolver.setSuffix(".jsp");
	    resolver.setViewClass(JstlView.class);
	    return resolver;
	}
}

 

자, 모든 준비는 완료가 되었습니다. 이제 pom.xml에 다음과 같이 부트스트랩 의존성을 추가해줍니다.

		<!-- Bootstrap CSS -->
		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>bootstrap</artifactId>
			<version>4.5.0</version>
		</dependency>

 

그리고 test.jsp 파일을 열어 아래와 같이 부트스트랩을 링크걸어줍니다.

<link rel="stylesheet" href="/webjars/bootstrap/4.5.0/css/bootstrap.min.css" />

 

만약 부트스트랩이 아닌 jQuery와 같은 javascript 라이브러리를 사용하고자 할 경우 의존성 추가는 동일하게, jsp파일에 링크를 걸어줄 때에는 아래와 같은 형태로 해주시면 됩니다.

<script src="/webjars/jqeury/3.5.1/js/jqeury.min.js"></script>

 

자, 여기까지 완료되면 모든 준비는 끝난것입니다. 이제 부트스트랩에서 제공하는 기능을 마음껏 사용하시면 됩니다.

저는 지난 시간에 최종결과물에서 버튼을 하나 추가해보는 정도로 이번 포스팅을 마치겠습니다.

test.jsp의 body에 다음 한 줄을 추가했습니다.

<button type="button" class="btn btn-danger btn-lg">버튼</button>

 

그리고 결과를 확인해보면 아래와 같이 빨간색 버튼이 추가된 것을 확인할 수 있습니다.

 

이상으로 Eclipse + SpringBoot + JSP 개발환경 세팅하기 2탄 포스팅을 마칩니다.

오늘도 즐겁고 행복한 코딩을 하는 케이치였습니다~

💻 Programming/Java

이클립스에 스프링툴즈(STS) 설치하기

이클립스 스프링툴즈 설치방법

이번에는 이클립스에서 스프링부트를 사용하기위한 스프링툴즈 설치방법을 알아볼게요.

이클립스가 설치완료되었다는 가정하에 아래 순서대로 진행하시면됩니다.

 

1. 이클립스 상단 메뉴에서 Help > Eclipse Marketplace... 선택합니다.

 

 

2. 검색창에 spring 으로 검색한 뒤 제일 상단에 Spring Tools (aka Spring Tools Suite) 가 보이면 install 버튼 클릭 (2020년 7월 현재 최신 버전은 4.7.0.RELEASE)

만약 4.7.0 버전이 보이지 않는다면 이클립스 버전이 오래된 건 아닌지 확인해보시고 최신 버전으로 이클립스를 다시 다운받아 설치하신 뒤 진행하시기 바랍니다.

 

3. required 항목만 남기고 나머지는 체크 해제합니다.

 

4. 라이센스 동의를 선택하고 Finish 버튼을 클릭합니다.

이상으로 이클립스에서 스프링부트를 쉽게 사용할 수 있도록 해주는 유용한 플러그인 스프링 툴즈 설치방법에 대한 포스팅을 마칩니다.

이클립스에서 스프링부트 기반으로 JSP개발하기

안녕하세요, 오늘은 스프링부트 기반으로 JSP개발을 할 수 있는 환경을 세팅하는 과정을 알려드리겠습니다.

최근에 대규모의 웹 개발은 백엔드와 프론트엔드를 구분해서 별도의 프로젝트로 구성하거나 모듈로 나누어 개발을 진행하고 있습니다.

어떻게 개발을 하던지 결국 백엔드와 프론트엔드를 완전히 분리하여 개발하게되죠. 하지만 그렇게까지 할 필요가 없는 작은 프로젝트들은 굳이 그렇게 나누어 개발을 할 필요가 없습니다. 그렇게 구분을 하는 것이 오히려 유지보수를 힘들게 하는 원인이 되기도 하죠.

오늘은 하나의 스프링부트 프로젝트를 만들고 JSP를 사용할 수 있도록 세팅하는 부분까지 알려드립니다.

개발에 필요한 준비물은 아래와 같습니다.

 

1. Eclipse (2020년 7월 기준 최신버전 다운로드)

Version: 2020-09 M1 (4.17.0 M1)

 

2. Spring Tools plugin (설치방법)

 

이렇게만 있으면 일단 준비는 완료입니다.

 

이제 새로운 프로젝트를 생성합니다. 현재 이클립스를 처음 설치하신 분이라면 패키지 탐색기(Project Explorer)에 아래와 같이 나오는데 여기서 밑에서 두 번째에 있는 Create a project...를 선택합니다. 만약 이미 프로젝트를 만들어 놓은게 있는 분들이라면 그냥 탐색기 창에서 우클릭해서 New > Project 를 선택하시면 됩니다.

아래와 같이 새 프로젝트 생성 마법사가 뜨면 spring 으로 검색을 해서 Spring Starter Project를 선택합니다.

 

이제 만들 프로젝트의 이름을 Name 항목에 적어줍니다. 그리고 Java Version 은 11로 선택해줍니다. (8로 해도 무방합니다)

 

Spring Boot의 버전을 선택할 수 있는데 이 부분은 그대로 놔두고 Available 검색창에서 web이라고 검색하여 Spring Web을 선택해줍니다. (이외에도 lombok이나 데이터 베이스 드라이버, MyBatis 등 유용한 기능들을 선택하여 사전설치가 가능합니다만 여기서는 선택하지 않습니다.)

 

Finish 버튼을 누르면 아래와 같은 구조를 갖는 프로젝트를 생성해줍니다.

이제 MyDemoApplication.java 파일을 열어 아래와 같이 수정해줍니다.

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

@SpringBootApplication
public class MyDemoApplication extends SpringBootServletInitializer {

	@Override
	protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
		return application.sources(MyDemoApplication.class);
	}

	public static void main(String[] args) {
		SpringApplication.run(MyDemoApplication.class, args);
	}

}

SpringBootServletInitializer를 상속하고 configure 메서드를 오버라이드하였습니다.

 

그리고 pom.xml 파일에 아래 의존성을 추가해줍니다. jasper는 JSP 파일을 컴파일 해주는 기능을 합니다.

<!-- Need this to compile JSP -->
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <scope>provided</scope>
</dependency>

 

이제 프로젝트명에서 우클릭하고 "src/main/webapp" 새 폴더를 하나 만듭니다. 이렇게 폴더를 만들면 아래와 같이 생성됩니다.

 

이제 다시 우클릭하여 build path 설정(Configure Buildpath...)화면으로 들어갑니다.

 

Add Folder... 를 클릭하고 webapp 디렉토리를 찾아 체크해줍니다.

OK 버튼을 누르고 Apply하면 프로젝트 구조가 아래와 같이 바뀐것을 확인할 수 있습니다.

이제 webapp 아래에 WEB-INF디렉토리를 생성하고 하위에 또 views 라는 디렉토리를 생성합니다. 그리고 test.jsp 파일을 만들어 아래와 같이 작성해줍니다.

<%@ page import="java.util.*" %>

<!DOCTYPE html>
<html>
<body>
	<h1>Test Page</h1>
	Today's date: <%= new Date() %>
</body>
</html>

 

application.properties 파일에는 다음과 같이 두 라인을 추가해줍니다.

spring.mvc.view.prefix = /WEB-INF/views/
spring.mvc.view.suffix = .jsp

 

자, 이제 마지막으로 해당 페이지와 연결할 API를 작성합니다. root package에서 controller 라는 패키지를 하나 만들고 그 아래에 DemoController.java를 아래와 같이 작성합니다.

package com.example.demo.controller;


import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;


@Controller
public class DemoController {
	
	@GetMapping("/test")
	public String login() {
		return "/test";
	}
}

 

여기까지 완료되었으면 이제 Run As > Java Application 또는 Run As > Spring Boot App 으로 기동시켜줍니다.

그리고 localhost:8080/test 에 접속하면 아래와 같이 jsp 페이지가 뜨는 것을 확인할 수 있습니다.

 

JSP 페이지에 javascript 및 css 파일 연동

이제 JSP 페이지에 javascript 파일 및 css 파일을 연결시켜보겠습니다.

javascript와 css 파일은 src/main/resources 하위의 static 폴더 안쪽에 몰아넣어주면 됩니다.

우선 static 폴더 하위에 script 폴더를 만들어 test.js파일을 생성하고 아래 내용을 작성합니다.

$(document).ready(function() { printCurrentDatetime(); });

function printCurrentDatetime() {
	let date = new Date();
	$('#currentTime').html(date);
	setTimeout(printCurrentDatetime, 1000);
}

 

그리고 static 폴더 하위에 style 폴더를 만들어 test.css 파일을 생성하고 아래와 같이 작성합니다.

@charset "UTF-8";

body {background-color:cornflowerblue;}

 

자, 더이상 파일을 만들 필요는 없습니다. 마지막으로 JSP 파일에 위에서 작성한 두 파일을 연결시켜주겠습니다.

test.jsp 파일을 열어 아래와 같이 수정해줍니다.

<%@ page import="java.util.*"%>

<!DOCTYPE html>
<html>
<head>

<!-- JS link -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="script/test.js"></script>

<!-- CSS link -->
<link href="style/test.css" rel="stylesheet">

</head>
<body>
	<h1>Test Page</h1>
	Today's date: <span id='currentTime'></span>
</body>
</html>

 

변경한 내용은 다음과 같습니다.

1. html의 body 안에 있던 스크립트 코드를 test.js로 옮기면서 refresh 기능을 추가

2. test.js파일에서 jquery를 사용하기 때문에 html페이지(jsp파일)에 jQuery 라이브러리 링크 추가

3. css 링크를 추가하고 body의 백그라운드 색상을 cornflowerblue 로 설정

 

여기까지 작업이 완료되면 최종적으로 아래와 같은 패키지 구조를 갖게됩니다.

SpringBoot + JSP 연동 최종 프로젝트 구조

 

여기까지 잘 따라오셨다면 실행시켰을 때 아래와 같이 현재시간이 계속 업데이트되는 파란 화면을 볼 수 있습니다. 😊

 

최종 완료 화면

 

이상으로 Eclipse에서 SpringBoot와 JSP를 연동하여 웹프로젝트를 구성하는 방법을 알아보았습니다.

 

스프링 RestTemplate 타임아웃 설정을 하는데 타임아웃 시간이 설정한대로 적용되지 않는 듯 하여 테스트 해봄..

보통 HttpComponentsClientHttpRequestFactory 와 SimpleClientHttpRequestFactory 를 사용하여 설정을 함

스프링의 RestTemplate 기본적으로 SimpleClientHttpRequestFactory를 사용함

SimpleClientHttpRequestFactory를 이용해서 설정을 하면 정상적으로 세팅한 값에 타임아웃이 발생함.

하지만 HttpComponentsClientHttpRequestFactory 를 이용하면 설정한 시간보다 4배 긴 시간이 흐른 뒤에야 타임아웃이 발생하였음

@Test
public void 타임아웃_테스트() {

HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setReadTimeout((int) TimeUnit.SECONDS.toMillis(10));
factory.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(5)); // 4배의 시간이 걸린 뒤에야 타임아웃 발생
factory.setConnectionRequestTimeout(5 * 1000);

// SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
// factory.setReadTimeout((int) TimeUnit.SECONDS.toMillis(10));
// factory.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(5)); // 세팅한 시간대로 타임아웃 발생

// RequestConfig config = RequestConfig.custom()
// .setSocketTimeout((int) TimeUnit.SECONDS.toMillis(1))
// .setConnectTimeout((int) TimeUnit.SECONDS.toMillis(5))
// .setConnectionRequestTimeout((int) TimeUnit.SECONDS.toMillis(10))
// .build();
// CloseableHttpClient client = HttpClientBuilder
// .create()
// .setDefaultRequestConfig(config)
// .build();
// HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(client);

RestTemplate restTemplate = new RestTemplate(factory);

// 1. connect timeout 테스트
long start = System.currentTimeMillis();
try {
ResponseEntity<String> result = restTemplate.getForEntity("https://abc.com:81/test", String.class);
} catch (ResourceAccessException e) {
log.error("타임아웃!! {}", TimeUtils.printDuration(System.currentTimeMillis() - start), e);
}

}


아직 원인은 확인하지 못함.


💻 Programming

스프링부트 스케쥴러 사용하기

스프링부트 스케쥴러

이번 포스팅에서는 스프링부트 프로젝트에서 스케쥴링 작업을 등록하여 사용하는 방법에 대해서 설명합니다.

스프링 부트에서 스케쥴러를 사용하려면 우선 아래처럼

@EnableScheduling 어노테이션을 @Configuration이 붙은 클래스에 등록해줘야 합니다.

 

스케쥴링 활성화를 위한 @EnableScheduling 사용

 

위 처럼 어노테이션을 붙여주면

스프링이 관리하는 빈 중에서 아래와 같이

@Scheduled 어노테이션이 붙어있는 것들을

찾아서 활성화 시켜주는 역할을 하게됩니다.

 

위 처럼 @Scheduled 어노테이션을 붙여주고 실행주기를 특정지어주면

해당 애플리케이션이 실행되면 자동으로 실행이 됩니다.

 

실행주기를 설정하는 방법은 기본적으로 세 가지가 있습니다.

 

fixedRate : 매 특정 밀리초마다 동작

fixedDelay : 해당 기능이 종료된 후 특정 밀리초 이후에 다시 동작

cron : 크론표현식에 정해진 내용대로 동작

 

여러 개의 스케쥴 잡을 등록할 때에는 주의할 점이 있습니다.

위 처럼 두 개의 스케쥴 잡이 등록되어있을 경우 한 번에 하나의 잡만 동작한다는 점입니다.

즉, @Scheduled가 붙은 기능들이 실행되는 시간이 중복될 경우 한 번에 하나의 작업만 실행됩니다.

 

만약 두 개 이상의 기능이 한꺼번에 실행되어도 성능에 문제가 없을 경우에는

아래처럼 @Async처리를 할 수 있습니다.

 

@Async 처리한 스케쥴 tasks

 

물론, @Async 가 적용되려면 @Configuration에 @EnableAsync도 추가해줘야 합니다.

 

여기까지 설정이 되었다면 위 두 scheduled task들은 동시에 실행됩니다.

 

추가로 @Async를 붙이지 않은 task를 하나 더 추가하면 어떻게 동작할까요?

task를 하나 더 추가해서 꼭!! 테스트해보세요.

 

이상으로 스프링부트에서 스케쥴링 잡을 등록하여 활용하는 방법에 대해 간단히 포스팅 해보았습니다.

유용했다면 좋아요 꾹!꾹!꾹! 눌러주세요 ^-^