본문 바로가기
Spring/SpringBoot

[SpringBoot] 다중 DB 및 다중 개발환경에서 JNDI 설정

by J4J 2021. 5. 25.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 다중 DB 및 다중 개발환경에서 JNDI 설정하는 방법에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

이전 포스팅에서는 jndi를 이용한 가장 기본적인 환경 구성에 대해 다뤘었습니다.

 

이번에는 한 번에 여러 개의 데이터베이스에도 연결되고 또한 동일한 코드로 개발/운영 환경에 맞는 데이터베이스 접속될 수 있도록 설정을 해보겠습니다.

 

다중 DB 같은 경우는 jndi 설정을 위한 resource를 여러 개를 등록한 뒤 각각의 resource와 매핑되는 rootContext 설정을 해 볼 예정입니다.

 

개발/운영 환경을 구분하는 것은 profile을 이용하여 설정해 볼 예정입니다.

 

 

 

참고적으로 설정 부분만 끄집어내기에는 애매하다고 생각되어서 MyBatis를 이용하여 설정하는 방법을 A-Z까지 보여드리도록 하겠습니다.

 

 

데이터베이스 설정

 

-- 개발 jndi1
create database devjndi1;

use devjndi1;

create table student (
    name varchar(50),
    age int,
    score int
);

insert into student values ('학생 가', 13, 68);
insert into student values ('학생 나', 14, 63);

-- 개발 jndi2
create database devjndi2;

use devjndi2;

create table school (
    name varchar(50),
    region varchar(50),
    ranking int
);

insert into school values ('학교 가', '서울', 1);
insert into school values ('학교 나', '경기', 2);

-- 운영 jndi1
create database prodjndi1;

use prodjndi1;

create table student (
    name varchar(50),
    age int,
    score int
);

select * from student;

insert into student values ('student a', 10, 35);
insert into student values ('student b', 17, 89);

-- 운영 jndi2
create database prodjndi2;

use prodjndi2;

create table school (
    name varchar(50),
    region varchar(50),
    ranking int
);

insert into school values ('school a', 'LA', 1);
insert into school values ('school b', 'NY', 2);

 

 

프로젝트 생성 및 maven 설정

 

[ 1. starter project 생성 정보 ]

 

starter project 생성 정보

 

 

[ 2. Next 클릭 후 dependency 추가 ]

 

dependency 추가

 

 

MyBatis가 갑자기 체크가 안돼서 생성하고 따로 추가하도록 하겠습니다.

 

 

[ 3. pom.xml에 dependency 추가 ]

 

<!-- MyBatis -->
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>2.1.4</version>
</dependency>

<!-- jndi설정을 위한 dbcp -->
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-dbcp2</artifactId>
</dependency>

 

 

profile 설정

 

[ 1. application-dev.properties 생성 (classpath:application-dev.properties) ]

 

# 만들기만 해주면 됩니다.

 

 

[ 2. application-prod.properties 생성 (classpath:application-prod.properties) ]

 

# 만들기만 해주면 됩니다.

 

 

config 파일

 

[ 1. 개발환경에 사용되는 DB Resource 등록 (com.spring.jndi.config.devDataSource) ]

 

package com.spring.jndi.config;

import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;
import org.apache.tomcat.util.descriptor.web.ContextResource;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.embedded.tomcat.TomcatWebServer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
@Profile("dev") // dev profile에만 적용
public class devDataSource {
	
	@Bean
	public TomcatServletWebServerFactory tomcatFactory() {
		return new TomcatServletWebServerFactory() {
			@Override
			protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
				tomcat.enableNaming();
				return super.getTomcatWebServer(tomcat);
			}
			
			@Override
			protected void postProcessContext(Context context) {
				// 개발 jndi1
				context.getNamingResources().addResource(getResource("jndi1", "com.mysql.cj.jdbc.Driver", "jdbc:mysql://localhost:3306/devjndi1?serverTimezone=UTC", "root", "root"));
				
				// 개발 jndi2
				context.getNamingResources().addResource(getResource("jndi2", "com.mysql.cj.jdbc.Driver", "jdbc:mysql://localhost:3306/devjndi2?serverTimezone=UTC", "root", "root")); 
			}
		};
	}
	
	public ContextResource getResource(String name, String driverClassName, String url, String username, String password) {
		ContextResource resource = new ContextResource();
		resource.setName(name); // 사용될 jndi 이름
		resource.setType("javax.sql.DataSource");
		resource.setAuth("Container");
		resource.setProperty("factory", "org.apache.commons.dbcp2.BasicDataSourceFactory");
		
		// datasource 정보
		resource.setProperty("driverClassName", driverClassName);
		resource.setProperty("url", url);
		resource.setProperty("username", username);
		resource.setProperty("password", password);
		
		return resource;
	}
}

 

 

[ 2. 운영환경에 사용되는 DB Resource 등록 (com.spring.jndi.config.prodDateSource) ]

 

package com.spring.jndi.config;

import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;
import org.apache.tomcat.util.descriptor.web.ContextResource;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.embedded.tomcat.TomcatWebServer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
@Profile("prod") // prod profile에만 적용
public class prodDataSource {
	
	@Bean
	public TomcatServletWebServerFactory tomcatFactory() {
		return new TomcatServletWebServerFactory() {
			@Override
			protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
				tomcat.enableNaming();
				return super.getTomcatWebServer(tomcat);
			}
			
			@Override
			protected void postProcessContext(Context context) {
				// 운영 jndi1
				context.getNamingResources().addResource(getResource("jndi1", "com.mysql.cj.jdbc.Driver", "jdbc:mysql://localhost:3306/prodjndi1?serverTimezone=UTC", "root", "root"));
				
				// 운영 jndi2
				context.getNamingResources().addResource(getResource("jndi2", "com.mysql.cj.jdbc.Driver", "jdbc:mysql://localhost:3306/prodjndi2?serverTimezone=UTC", "root", "root")); 
			}
		};
	}
	
	public ContextResource getResource(String name, String driverClassName, String url, String username, String password) {
		ContextResource resource = new ContextResource();
		resource.setName(name); // 사용될 jndi 이름
		resource.setType("javax.sql.DataSource");
		resource.setAuth("Container");
		resource.setProperty("factory", "org.apache.commons.dbcp2.BasicDataSourceFactory");
		
		// datasource 정보
		resource.setProperty("driverClassName", driverClassName);
		resource.setProperty("url", url);
		resource.setProperty("username", username);
		resource.setProperty("password", password);
		
		return resource;
	}
}

 

 

반응형

 

 

[ 3. jndi1 RootContext 설정 (com.spring.jndi.config.Jndi1RootContext) ]

 

package com.spring.jndi.config;

import java.io.IOException;

import javax.sql.DataSource;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@MapperScan(basePackages = {"com.spring.jndi1.**.repository"} /* 사용될 repository 경로 */, sqlSessionFactoryRef = "jndi1SqlSessionFactory")
@ComponentScan(basePackages = {"com.spring.jndi1.**.service"}) // 사용될 service 경로 
@EnableTransactionManagement
public class Jndi1RootContext {

	// dataSource 설정
	@Bean
	public DataSource jndi1DataSource() {
		JndiDataSourceLookup jndiDataSourceLookup = new JndiDataSourceLookup();
		return jndiDataSourceLookup.getDataSource("java:comp/env/jndi1"); // java:comp/env/{jndi이름}
	}
	
	// sqlSessionFactory 설정
	@Bean
	public SqlSessionFactoryBean jndi1SqlSessionFactory() throws IOException {
		SqlSessionFactoryBean jndi1SqlSessionFactory = new SqlSessionFactoryBean();
		jndi1SqlSessionFactory.setDataSource(jndi1DataSource());
		jndi1SqlSessionFactory.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:mybatis-config.xml")); // mybatis 설정 파일
		jndi1SqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/jndi1/**/*.xml")); // 사용될 mapper 경로
		
		return jndi1SqlSessionFactory;
	}
	
	// transaction 설정
	@Bean
	PlatformTransactionManager jndi1TransactionManager() {
		DataSourceTransactionManager jndi1TransactionManager = new DataSourceTransactionManager();
		jndi1TransactionManager.setDataSource(jndi1DataSource());
		
		return jndi1TransactionManager;
	}
}

 

 

[ 4. jndi2 RootContext 설정 (com.spring.jndi.config.Jndi2RootContext) ]

 

package com.spring.jndi.config;

import java.io.IOException;

import javax.sql.DataSource;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@MapperScan(basePackages = {"com.spring.jndi2.**.repository"} /* 사용될 repository 경로 */, sqlSessionFactoryRef = "jndi2SqlSessionFactory")
@ComponentScan(basePackages = {"com.spring.jndi2.**.service"}) // 사용될 service 경로
@EnableTransactionManagement
public class Jndi2RootContext {
	
	// dataSource 설정
	@Bean
	public DataSource jndi2DataSource() {
		JndiDataSourceLookup jndiDataSourceLookup = new JndiDataSourceLookup();
		return jndiDataSourceLookup.getDataSource("java:comp/env/jndi2"); // java:comp/env/{jndi이름}
	}
	
	// sqlSessionFactory 설정
	@Bean
	public SqlSessionFactoryBean jndi2SqlSessionFactory() throws IOException {
		SqlSessionFactoryBean jndi2SqlSessionFactory = new SqlSessionFactoryBean();
		jndi2SqlSessionFactory.setDataSource(jndi2DataSource());
		jndi2SqlSessionFactory.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:mybatis-config.xml")); // mybatis 설정 파일
		jndi2SqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/jndi2/**/*.xml")); // 사용될 mapper 경로
		
		return jndi2SqlSessionFactory;
	}
	
	// transaction 설정
	@Bean
	PlatformTransactionManager jndi2TransactionManager() {
		DataSourceTransactionManager jndi2TransactionManager = new DataSourceTransactionManager();
		jndi2TransactionManager.setDataSource(jndi2DataSource());
		
		return jndi2TransactionManager;
	}
}

 

 

MyBatis 설정 및 Mapper 파일

 

[ 1. MyBatis 설정 파일 (classpath:mybatis-config.xml) ]

 

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<settings>
		<setting name="mapUnderscoreToCamelCase" value="true"/> <!-- 카멜케이스 적용 -->
	</settings>

	<!-- alias 등록 -->
	<typeAliases>
		<typeAlias type="com.spring.jndi1.dto.StudentDto" alias="student"/>
		<typeAlias type="com.spring.jndi2.dto.SchoolDto" alias="school"/>
	</typeAliases>
</configuration>

 

 

 

[ 2. jnid1에서 사용되는 student mapper (classpath:mapper/jndi1/studentmapper.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.spring.jndi1.repository.StudentRepository">
	<insert id="insert" parameterType="student">
		insert
		into student
		values (#{name}, #{age}, #{score})
	</insert>
	
	<select id="selectAll" resultType="student">
		select *
		from student
	</select>
</mapper>

 

 

[ 3. jnid2에서 사용되는 school mapper (classpath:mapper/jndi2/schoolmapper.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.spring.jndi2.repository.SchoolRepository">
	<insert id="insert" parameterType="school">
		insert
		into school
		values (#{name}, #{region}, #{ranking})
	</insert>
	
	<select id="selectAll" resultType="school">
		select *
		from school
	</select>
</mapper>

 

 

728x90

 

 

DTO 및 Repository 파일

 

[ 1. jndi1에서 사용되는 student dto (com.spring.jndi1.dto.StudentDto) ]

 

package com.spring.jndi1.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class StudentDto {
	private String name;
	private int age;
	private int score;
}

 

 

[ 2. jndi1에서 사용되는 student repository (com.spring.jndi1.repository.StudentRepository) ]

 

package com.spring.jndi1.repository;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

import com.spring.jndi1.dto.StudentDto;

@Mapper
public interface StudentRepository {
	
	public int insert(StudentDto studentDto);
	
	public List<StudentDto> selectAll();
}

 

 

[ 3. jndi2에서 사용되는 school dto (com.spring.jndi2.dto.SchoolDto) ]

 

package com.spring.jndi2.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SchoolDto {
	private String name;
	private String region;
	private int ranking;
}

 

 

[ 4. jndi2에서 사용되는 school repository (com.spring.jndi2.repository.SchoolRepository) ]

 

package com.spring.jndi2.repository;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

import com.spring.jndi2.dto.SchoolDto;

@Mapper
public interface SchoolRepository {
	
	public int insert(SchoolDto schoolDto);
	
	public List<SchoolDto> selectAll();
}

 

 

Service 파일

 

service단 같은 경우는 서로 구분되어 있는 jndi 환경에서 서로 다른 transaction 처리를 해줘야 합니다.

 

그렇기 때문에 저 같은 경우는 jndi1과 jndi2에서 service처리를 각각 해주고 jndi1과 jndi2가 통합적으로 모이는 service단으로 추가적으로 만들어 사용하는 편입니다.

 

구현하는 방법은 여러가지가 있기 때문에 다른 방법도 찾아보시길 추천드립니다.

 

 

[ 1. jndi1에서 사용되는 student service interface (com.spring.jndi1.service.StudentService) ]

 

package com.spring.jndi1.service;

import java.util.List;

import com.spring.jndi1.dto.StudentDto;

public interface StudentService {
	
	public int insertStudent(); // student 생성
	
	public List<StudentDto> selectStudentAll(); // student 조회
}

 

 

[ 2. 위의 service의 구현체 (com.spring.jndi1.service.StudentServiceImpl) ]

 

package com.spring.jndi1.service;

import java.util.List;

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

import com.spring.jndi1.dto.StudentDto;
import com.spring.jndi1.repository.StudentRepository;

@Service
@Transactional(transactionManager = "jndi1TransactionManager") // jndi1에 해당하는 transaction
public class StudentServiceImpl implements StudentService {

	@Autowired
	StudentRepository studentRepository;
	
	@Override
	public int insertStudent() {
		return studentRepository.insert(new StudentDto("히", 1, 2)) + studentRepository.insert1(new StudentDto("비", 1, 2));
	}
	
	@Override
	public List<StudentDto> selectStudentAll() {
		return studentRepository.selectAll();
	}	
}

 

 

[ 3. jndi2에서 사용되는 school service interface (com.spring.jndi2.service.SchoolService) ]

 

package com.spring.jndi2.service;

import java.util.List;

import com.spring.jndi2.dto.SchoolDto;

public interface SchoolService {
	
	public int insertSchool(); // school 생성
	
	public List<SchoolDto> selectSchoolAll(); // school 조회
}

 

 

[ 4. 위의 service의 구현체 (com.spring.jndi2.service.SchoolServiceImpl) ]

 

package com.spring.jndi2.service;

import java.util.List;

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

import com.spring.jndi2.dto.SchoolDto;
import com.spring.jndi2.repository.SchoolRepository;

@Service
@Transactional(transactionManager = "jndi2TransactionManager") // jndi2에 해당하는 transaction
public class SchoolServiceImpl implements SchoolService {
	
	@Autowired
	SchoolRepository schoolRepository;
	
	@Override
	public int insertSchool() {
		return schoolRepository.insert(new SchoolDto("로", "라", 3));
	}
	
	@Override
	public List<SchoolDto> selectSchoolAll() {
		return schoolRepository.selectAll();
	}
}

 

 

 

 

[ 5. jndi1과 jndi2가 통합적으로 사용되는 education service interface (com.spring.jndi.service.EducationService) ]

 

package com.spring.jndi.service;

import java.util.List;

import com.spring.jndi1.dto.StudentDto;
import com.spring.jndi2.dto.SchoolDto;

public interface EducationService {

	public List<StudentDto> selectStudentAll(); // student 조회
	
	public List<SchoolDto> selectSchoolAll(); // school 조회
	
	public int insertAll(); // student, school 생성
}

 

 

[ 6. 위의 service의 구현체 (com.spring.jndi.service.EducationServiceImpl) ]

 

package com.spring.jndi.service;

import java.util.List;

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

import com.spring.jndi1.dto.StudentDto;
import com.spring.jndi1.service.StudentService;
import com.spring.jndi2.dto.SchoolDto;
import com.spring.jndi2.service.SchoolService;

@Service
public class EducationServiceImpl implements EducationService {
	
	@Autowired
	StudentService studentService;
	
	@Autowired
	SchoolService schoolService;
	
	@Override
	public List<StudentDto> selectStudentAll() {
		return studentService.selectStudentAll();
	}
	
	@Override
	public List<SchoolDto> selectSchoolAll() {
		return schoolService.selectSchoolAll();
	}
	
	@Override
	public int insertAll() { // student와 school의 transaction 처리가 다름
		return studentService.insertStudent() + schoolService.insertSchool();
	}
}

 

 

Controller 파일

 

controller단은 단순하게 student와 school데이터를 확인할 수 있도록 해보겠습니다. (com.spring.jndi.controller.EducationController)

 

package com.spring.jndi.controller;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.spring.jndi.service.EducationService;

@RestController
public class EducationController {
	
	@Autowired
	EducationService educationService;
	
	@GetMapping("/selectAll")
	public ResponseEntity<Object> selectAll() {
		Map<String, Object> resultMap = new HashMap<>();
		
		resultMap.put("student", educationService.selectStudentAll());
		resultMap.put("school", educationService.selectSchoolAll());
		
		return new ResponseEntity<Object>(resultMap, HttpStatus.OK);
	}
}

 

 

개발 환경 테스트

 

[ 1. profile 설정하여 프로젝트 실행 (프로젝트 우 클릭 → Run as → Run Configurations...) ]

 

개발 profile 설정 후 실행

 

 

[ 2. 포스트맨을 이용하여 데이터 조회 ]

 

개발 데이터 조회

 

 

운영 환경 테스트

 

[ 1. profile 설정하여 프로젝트 실행 ]

 

운영 profile 설정 후 실행

 

 

[ 2. 포스트맨을 이용하여 데이터 조회 ]

 

운영 데이터 조회

 

 

파일 구성

 

파일 구성

 

 

 

 

 

이상으로 다중 DB 및 다중 개발환경에서 JNDI 설정하는 방법에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

728x90
반응형

댓글