반응형

1. 개요

 이번 동계 올림픽 하이라이트를 보니 '쭝' 얘기가 많더라. 그러고보니 한창 배그에 빠져있을 때였나... 그날도 회사갔다가 친구들이랑 배그를 하려고 접속했더니 XING, MING 으로 시작하는 '쭝' 사람들과 필자의 아이디가 4인 스쿼드를 돌리고 있더라. 나쁜 X끼들.

 

 본론으로 넘어가겠다. 서비스 오픈 후 국가별 서비스 접근 내역을 확인했다. 우리나라가 대부분이나 타국(중국, 미국, 러시아, 체코 기타등등...)에서의 접근도 있었다.

 타국 사용자를 타겟으로 한 서비스가 아니었기에 신경을 쓰지 않았으나, 필자의 서버는 호스팅 서버에 묶여있었고, 트래픽에 따른 추가비용 결제가 걸려있는 호스팅 서버의 특성 상 불필요한 접근을 막아야 했다. 타국에서의 접근은 불순한 의도일 확률이 높기 때문에 보안을 위해서도 해외 IP를 차단해야했다는 생각이 들었다.

 해외 IP를 차단하는 방법에 대해 서치를해보니 GeoIP를 사용하는 방법이 많던데, 몇천건 이상으로는 유료라는 썰이 있어 KISA의 무료 오픈 API인 WHOIS를 채택하게 되었다.


2. WHOIS API

https://whois.kisa.or.kr/kor/openkey/keyCre.do

 

KISA 후이즈검색 whois.kisa.or.kr

한국인터넷진흥원 인터넷주소자원 검색(후이즈검색) 서비스 입니다.

xn--c79as89aj0e29b77z.xn--3e0b707e

 

 WHOIS API는 KISA에서 제공하는 무료 Open API로 IP에 대한 국가코드 값을 얻을 수 있다. OpenAPI 사용 안내를 보면 다음과 같이 IP주소/AS 번호에 대한 국가코드를 요청하는 API를 확인할 수 있다.

 무료라는 큰 장점이 있지만, API를 위해 서버 내에서 HTTP 통신을 한번 태워야한다는 단점이 있다.

 * 만약, 더 좋은 방법이 있다면 댓글로 공유부탁해요!

WHOIS API

 

WHOIS API 응답값


3. 키 발급

 API 사용을 위해서는 먼저 키를 발급받아야 한다. WHOIS API 사이트에 접속 후 전자우편을 통해 발급받는다.

키 발급

 필자의 경우 네이버 메일로 받았으며, 다음과 같이 메일에 키 값이 포함되어 온다. @_@. 준비는 끝났다. (벌써)

WHOIS API 인증 키 메일


4. 구현

 필자는 단순 API 테스트를 위해 JUnit으로 단순하게 로직을 구현하였으며, 응답받은 Json String 값을 객체로 변환하기 위해 ObjectMapper를 사용하였다.

@Test
	public void whoisAPI() {

		String ip = "[IP]";
		String apiKey = "[API KEY]";
		String apiUri = "http://whois.kisa.or.kr/openapi/ipascc.jsp";
		
		List<NameValuePair> nameValuePairs= new ArrayList<NameValuePair>();
		
		nameValuePairs.add(new BasicNameValuePair("query",ip));
		nameValuePairs.add(new BasicNameValuePair("key",apiKey));
		nameValuePairs.add(new BasicNameValuePair("answer","json"));
		
		HttpGet httpGet = new HttpGet(apiUri);
		
		URI uri = null;
		ObjectMapper objectMapper = null;
		
		try {
			uri = new URIBuilder(httpGet.getURI())
					.addParameters(nameValuePairs)
					.build();
			
			httpGet.setURI(uri);
			
			CloseableHttpClient httpClient = HttpClientBuilder.create().build();
			CloseableHttpResponse response = httpClient.execute(httpGet);

			int statusCode = response.getStatusLine().getStatusCode();
			
			if(statusCode == HttpStatus.OK.value()) {
				String json = EntityUtils.toString(response.getEntity(), "UTF-8");
				objectMapper = new ObjectMapper();
				
				Map<String,Map<String,String>> map = objectMapper.readValue(json, Map.class);
				Map<String,String> whois = map.get("whois");
				
				System.out.println("response : "+ map.toString());
				System.out.println("contryCode :"+whois.get("countryCode"));
			}
		} catch (URISyntaxException e) {
			e.printStackTrace();
		} catch (ClientProtocolException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} 
	}

 


5. 실행 결과

 실행 결과 key,value 형태로 IP 및 국가코드가 들어있는 걸 확인할 수 있다. 너무 간단하쥬?

실행결과


6. 마치며

 다음 포스팅에서는 인터셉터를 활용하여 해외 IP일 경우 페이지 접근을 차단시키는 로직을 구현해보도록 하겠다. HttpClient 또한 ConnectionPool 한 HttpClient로 커스텀하여 사용해보도록 하겠다.

 

아참, KISA 홈페이지를 둘러보다 우연히 발견한건데, 이 API가 개방된지 얼마 되지않았더라. 많이 쓰세요 여러분.

될놈될...헤헤...

 

반응형
반응형

1. 개요

  • 토이 프로젝트 진행 중 관리자 권한에 따른 기능 분리가 필요하여 기존 Gradle 프로젝트에 SpringSecurity 인증을 끼얹어보았다.
  • 스프링 시큐리티 온라인 강의를 들은 적이 있다. 이해를 할때마다 고개를 끄덕거리며 만족해하던 내 자신이 기억났다. '생겨버렸다. 자신감'. 하지만 정작 기억해야할 건 전혀 기억하지 못하더라... 자신감은 잠시 버려두고 강의노트를 펼쳤다.

 


2. 환경

  • SpringBoot
  • Gradle
  • Mybatis

3. 스프링 시큐리티 인증 절차

  • 스프링 시큐리티를 사용하면 여러 인증 및 인가 필터를 거친다. 필터를 거칠때마다 이와 관련된 여러 인터페이스 구현체를 거치게 되는데, 이걸 우리의 서비스에 맞게 커스텀하면 된다. 그러려면? 내부적으로 어떤 클래스(절차)들을 거치는지 알아볼 필요가 있다. 
    SpringSecurity Form 인증 절차
     
  • UsernamePasswordAuthenticationFilter > AuthenticationManager > AuthenticationProvider > UserDetailsService > Repository(DB)
    1. UsernamePasswordAuthenticationFilter에서 요청값에 대한 Authentication 객체를 생성한다.
    2. 생성한 Authentication 객체를 AuthenticationManager에게 전달한다.
    3. AuthenticationManager는 실제 인증 처리를 하지 않고, 적절한 인증 처리 클래스에게 인증 처리를 위임하는, 말 그대로 매니저 역할을 하는 클래스이다. 폼 인증 요청에 대한 적절한 인증 처리 클래스인 AuthenticationProvider에게 Authentication 객체를 전달하여 인증 처리를 위임한다.
    4. AuthenticationProvider는 들어온 Authentication 객체의 username을 값을 추출하여 UserDetailsService의 loadUserByUsername 메서드 호출에 사용한다. 말 그대로 유저 ID에 대한 유저 정보를 조회한다.
    5. UserDetailsService는 username 값을 통해 DB에서 유저 상세 정보(아이디, 비밀번호, 권한 등)를  UserDetails 타입으로 조회한다.
    6. 조회한 UserDetilas 타입의 객체를 AuthenticationProvider에게 return한다.
    7. AuthenticationProvider는 DB에서 조회한 비밀번호와 요청으로 들어온 비밀번호를 체크한다. 만약 암호화 된 비밀번호라면 암호화 클래스를 사용해 체크한다.
    8. 비밀번호가 일치할 경우 인증 토큰에 들어갈 권한 객체인 authorities를 생성 후 원하는 권한을 넣어준다.
    9. AuthenticationProvider는 유저 정보 + authorities를 담은 Authentication 객체를 생성하여 AuthenticationManager에게 return한다.
    10. AuthenticationManager는 Authentication 객체를 UsernamePasswordAuthenticationFilter에게 return 한다.
    11. UsernamePasswordAuthenticationFilter는 Authentication 객체를 SecurityContext에 저장한다.

4. 구현

  • 필자가 정리한 위 인증 절차에서 커스텀해야할 녀석은 누구일까? 당연히 실질적인 인증처리를 하고있는 녀석들을 커스텀해야한다. AuthenticationProvider와 UserDetailsService가 핵심이다. 둘 다 인터페이스이므로 구현체를 생성하여 주입시켜주기만 하면 된다. SecurityConfig 부터 시작하자.

 

1) SecurityConfig.java

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
	
	@Autowired
	private AuthenticationSuccessHandler authenticationSuccessHandler;
	
	@Autowired
	private AuthenticationFailureHandler authenticationFailureHandler;
	
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {

		http
			.csrf().disable(); //일반 사용자에 대해 Session을 저장하지 않으므로 csrf을 disable 처리함.
		
		http
			.authorizeRequests()
			.antMatchers("/admin").permitAll()
			.antMatchers("/admin/**").hasRole("ADMIN")
			.antMatchers("/swagger-ui.html").hasRole("ADMIN")
			.antMatchers("/schedule/insert","/schedule/update","/schedule/delete").hasRole("ADMIN")
			.antMatchers("/comment/insert","/comment/update","/comment/delete").hasRole("ADMIN")
			.anyRequest().permitAll();
		
		http
			.formLogin()
			.loginPage("/admin")
			.loginProcessingUrl("/admin/login")
			.usernameParameter("username")
			.passwordParameter("password")
			.successHandler(authenticationSuccessHandler)
			.failureHandler(authenticationFailureHandler);
			
		http
			.logout()
			.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
			.logoutSuccessUrl("/admin")
			.invalidateHttpSession(true);
	
	}	
}

 - csrf().disable()

  •  먼저 csrf는 Cross Site Request Forgery의 약자로 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위를 특정 웹사이트에 요청하게하는 공격을 말한다.
  •  예를들어 은행 홈페이지에 특정 사용자에게 송금을 시키는 API(test.com/transMoney?money={송금할 금액}&to={송금받을 유저})가 있다고 하자.  A라는 공격자는 해당 홈페이지를 사용하는 유저들에게 다음과 같은 이미지 태그를 포함한 메일을 전송한다.
<img src="test.com/transMoney?money=100000&to=A />
  • 이 메일을 열어본 사용자는 src에 기입된 API가 호출되게 된다. 만약 해당 홈페이지에 로그인하여 세션이 브라우저에 남아있는 상태라면 공격자 A에게 100000원이 송금되는 것이다. 이런 공격을 csrf 공격이라고 한다.
  • http.csrf() 구문을 사용하면 클라이언트가 서버 통신 시 csrf 토큰 값을 함께 전달하고, 서버는 토큰에 대한 유효성을 체크하여 일치하지 않을 경우 요청을 차단하게 된다. 위 API를 호출한다 해도 csrf 토큰 값이 없으므로 서버에서는 401 에러가 발생할것이다. 하지만 내가 만든 서비스에서는 이러한 방식에 대한 필요성을 느끼지 못해 disable 처리를 하였다.

 

 - authorizeRequest() ~ anyRequest().permitAll()

  • 요청에 대한 인가를 설정한다. 현재 필자의 토이프로젝트 권한에 맞게 설정하였으므로 참고만 하고 넘어가면 된다.

 

 - formLogin()

  • formLogin 인증 방식을 사용한다는 의미이다. 

 

 - usernameParameter("username")와 passwordParameter("password")

  • 클라이언트가 전송한 폼 데이터 중 "username"과 "password"라는 name을 가진 값을 스프링 시큐리티에서 username, password로 사용한다. 즉, UsernamePasswordAuthenticationFilter 에서 Authentication 객체를 생성할때 각각의 변수 값을 사용한다.
<form action="/admin/login" id="form" method="post">
	<input type="password" name="password" id="adminPassword" />
    <input type="hidden" name="username" id="adminId" />
    <input type="submit" class="loginBtn" value="로그인">
</form>

 

 - loginPage()

  • 로그인할 페이지의 주소를 입력한다.

 

 - loginProcessingUrl()

  • 로그인을 처리할 Url을 입력한다.

 

 - successHandler()

  • 인증이 성공한 후 호출되는 핸들러 클래스이다.

  

 - failureHandler()

  • 인증이 실패한 후 호출되는 핸들러 클래스이다.

 

 - logout() ~ invalidateHttpSession()

  • /logout 을 호출하면 /admin 페이지로 이동하며 로그아웃이 되며, 이와 동시에 session이 무효화(invaildate)된다.

 

2) CustomAuthenticationSuccessHandler.java

@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler{

	private final Logger logger = LoggerFactory.getLogger(getClass());
	
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		
		logger.info("admin login Success !!");
		response.sendRedirect("/");
	}

}

 

  • AuthenticationSuccessHandler 인터페이스의 구현체이다.
  • 로그인이 성공할 경우 메인 페이지("/")로 리다이렉트 되도록 하였다.

 

3) CustomAuthenticationFailureHandler.java

@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler{

	private final Logger logger = LoggerFactory.getLogger(getClass());
	
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		
		logger.error("admin login Failed !!");
		response.sendRedirect("/admin?auth=fail");
	}

}
  • AuthenticationFailureHandler 인터페이스의 구현체이다.
  • 로그인이 실패할 경우 쿼리 스트링을 포함하여 "/admin"로 이동시켰다.
  • 참고로 /admin 페이지는 관리자 로그인을 시도하는 페이지이며, 클라이언트단에서 auth값에 따른 알림 메시지를 출력하기 위해 쿼리스트링을 포함시켰다.

 

4) CustomAuthenticationProvider.java

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider{

	@Autowired
	private UserDetailsService userDetailsService; //CustomUserDetails Class Autowired.
	
	@Autowired
	private PasswordEncoder passwordEncoder; //BCryptPasswordEncoder Class Autowired.
	
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		
		String username = authentication.getName();
		String password = (String)authentication.getCredentials();
		
		CustomUserDetails customUserDetails = (CustomUserDetails) userDetailsService.loadUserByUsername(username);
	
		if(!passwordEncoder.matches(password, customUserDetails.getPassword())) {
			throw new BadCredentialsException("비밀번호가 일치하지 않습니다.");
		}
				
		List<GrantedAuthority> authorities = new ArrayList<>();
		authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
		
		return new UsernamePasswordAuthenticationToken(username,password,authorities);

	}

	
	@Override
	public boolean supports(Class<?> authentication) {
		return authentication.equals(UsernamePasswordAuthenticationToken.class);
	}

}
  • AuthenticationProvider 인터페이스의 구현체이다.
  • 요청에 대한 ID와 PW 값이 포함된 Authentication이라는 객체가 요청 파라미터 값으로 들어온다. 인증절차의 4번째 단계이다.
  • authentication 객체에서 username 값을 사용하여 userDetailsService.loadUserByUsername 메서드를 호출한다.
  • 참고로 userDetailsService 구현체 내에서는 username을 가진 admin 계정의 정보를 조회하는 기능을 한다.
  • 비밀번호가 일치하지 않을 경우 BadCredentialsException을, 일치할 경우 GrantedAuthority 리스트 내에 ROLE_ADMIN 권한을 추가시킨 후 UsernamePasswordAuthenticationToken 생성자를 호출해 return한다. 이 생성자를 호출할 경우 내부적으로 AbstractAuthenticationToken 객체를 생성하는데, 이 객체는 Authentication 객체의 구현체이다. 즉, 인증절차의 9번째 단계이다.
  • supprotes 메서드는 authenticate 메서드의 동작 여부를 결정한다. false를 return하면 동작하지 않는다. 위 코드에서는 Form 인증에 대한 authentication 객체. 즉 UsernamePasswordAuthenticationToken 자료형으로 요청이 들어올 경우에만 인증 절차를 진행한다.

* 필자의 경우 DB에서 계정에 대한 권한 정보를 조회하여 부여해주는 방식이 아닌, 비밀번호 일치 시 ADMIN 권한을 주도록 로직이 짜여져있다. 이 부분은 전자와 같이 동작하도록 로직을 수정이 필요함을 느꼈다. 만약 독자분들도 전자와 같이 구현하고자한다면 loadUserByUsername 메서드를 통해 권한 정보도 조회하도록 쿼리 및 CustomUserDetails 클래스 수정 후 "ROLE_ADMIN" 부분에 customUserDetails의 get 메서드로 권한 정보를 가져와서 넣어주면 될 것이다. 

 

5) CustomUserDetailsService

@Service
public class CustomUserDetailsService implements UserDetailsService{

	@Autowired
	private AdminRepository adminRepository;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		
		UserDetails userDetails = adminRepository.getUserDetails(username);
		
		if(userDetails == null) {
			throw new UsernameNotFoundException("유효하지 않는 로그인 정보입니다.");
		}
		
		return userDetails;
	}

}
  • UserDetailsService 인터페이스의 구현체이다.
  • AdminRepository는 Mybatis에 대한 Mapper 인터페이스이다.
  • loadUserByUsername 메서드를 오버라이드한 후 AdminRepository 인터페이스를 사용해 admin 계정에 대한 id, pw 정보를 UserDetails 자료형으로 조회 후 리턴한다.

 

6) CustomUserDetails

@Component
public class CustomUserDetails implements UserDetails{

	private static final long serialVersionUID = 1L;

	private String username;
	private String password;
	
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public String getPassword() {
		return this.password;
	}

	@Override
	public String getUsername() {
		return this.username;
	}

	@Override
	public boolean isAccountNonExpired() {
		return false;
	}

	@Override
	public boolean isAccountNonLocked() {
		return false;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return false;
	}

	@Override
	public boolean isEnabled() {
		return false;
	}
}
  • UserDetails 인터페이스의 구현체이다.

 

 

7) AdminRepository

@Repository
public interface AdminRepository {

	public UserDetails getUserDetails(String username);
}


// AdminMapper.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.cds.repository.AdminRepository">

    <select id = "getUserDetails" resultType = "CustomUserDetails">
    	SELECT
    		id AS username
    		,password
    	FROM
    		TB_ADMIN
    	WHERE
    		id = #{username}
    		AND use_yn = 'Y'
    </select>
</mapper>
  • AdminRepository 및 AdminMapper 쿼리이다.
  • returnType의 CustomUserDetails은 typeAlias를 사용해 네이밍을 간소화시켰다.

5. 테스트

1) 로그인 실패 시 

로그인 실패 시 로그
로그인 실패 시 리다이렉트

2) 로그인 성공 시

로그인 실패 시 로그

 

로그인 성공 시 리다이렉트


6. 마치며

  • 스프링 시큐리티 폼 인증 절차에 대해 개념을 다시 정리할 수 있었던 좋은 기회였다. 또한 내가 작성한 로직들을 포스팅을 통해 재확인해보니 수정할 곳도 몇몇 보였다. 한동안 이핑계 저핑계로 포스팅을 하지 않아 쌓여있는 내용이 엄청 많아졌다. 천천히 조금씩 정리해나가야겠다.
반응형
반응형

1. 개요

  • Connection을 생성한 상태에서 Controller, Service, Repository, Mapper 구조를 통해 DB 데이터를 Select 하자
  • Controller, Service, Repository 클래스에 대한 자세한 설명은 하지 않겠다.

2. 준비

  • postgreSQL DB의 Select 테이블
  • mybatis 연동 경험

3. Controller 생성

  • 먼저 클라이언트의 요청을 받을 Controller 클래스를 생성한다.
  • 필자는 각각 Controller, Service, Repository 모두 각각의 패키지를 만든 후 생성했다.
  • test-select로 오는 get 요청에 대해 testService 인터페이스 구현체 응답값을 리턴하도록 하였다.
@Controller
public class TestController {

	@Autowired
	private TestService testService;
	
	@GetMapping("/test-select")
	@ResponseBody
	public List<BoardDto.Info> testSelect(){
		
		return testService.testSelect();
	}
}

4. ServiceInterface 생성

  • 서비스 인터페이스를 생성한다.
public interface TestService {

	public List<BoardDto.Info> testSelect();
}

5. Service 구현체 생성

  • TestService Interface의 구현체 클래스를 생성한다.
  • TestRepository를 Autowired 한다.
@Service
public class TestServiceImpl implements TestService{

	@Autowired
	private TestRepository testRepository;
	
	@Override
	public List<Info> testSelect() {

		return testRepository.testSelect();
	}

}

6. TestRepository 인터페이스 생성

  • 리포지토리 인터페이스를 생성한다.
  • 이 클래스는 인터페이스이기때문에 @Repository만 입력 시 구현체가 없으므로 Service 클래스에서 TestRepository를 주입에 실패했다는 에러가 발생한다. @Mapper 어노테이션은 이러한 인터페이스를 mybatis의 매퍼로 등록해주기 위해 사용된다. 즉 Mapper Bean이 되는것이다.
@Repository
@Mapper
public interface TestRepository {

	public List<Info> testSelect();
}

 

  • 하지만 이렇게 Mapper 어노테이션을 명시적으로 선언하게 되면 생성되는 모든 Repository에 다 넣어줘야한다. 이게 귀찮다면 DatabaseConfig 클래스에 @MapperScan("패키지 경로") 어노테이션을 선언해주자. 그럼 패키지 경로에 포함된 인터페이스에 대해 @Mapper 어노테이션을 선언한 효과를 얻을 수 있다.
@Configuration
@MapperScan("com.modu.repository")
public class DatabaseConfig {

	...
	
}

7. TestMapper 생성

  • Mapper.xml을 생성한다.
  • 필자의 경우 Dto 클래스를 static inner class 형식으로 사용하기 때문에 resultType에 $가 포함되어 있다. 만약, static inner class를 사용하지 않는다면 패키지 경로를 넣어주면 된다.
<?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.modu.repository.TestRepository">

	<select id="testSelect" resultType ="com.modu.dto.BoardDto$Info">
		SELECT
			board_seq
			,cat_id
			,title
			,content
			,writer
			,create_date
			,update_date
		FROM
			tbl_board
	</select>
</mapper>

 


8. 테스트

응답 값

 


 

9. 마치며

  • hikariCP와 mybatis, postgreSQL을 연동해보았는데 그저 머릿속에 있는 DB 통신에 대한 패턴을 별 생각없이 구현했다. DB 설정 클래스의 bean들은 각각 역할을 하는지, Mapper.xml 파일에 등록한 쿼리는 내부적으로 어떻게 생성되어 실제 쿼리가 처리되는지, Repository 인터페이스와 Mapper의 id가 어떻게 매핑되는지가 궁금해졌다. 다음 포스팅에서는 이런 설정 하나하나가 시스템적으로 어떻게 돌아가는지 알아봐야겠다.
반응형
반응형

1. 개요

 - SpringBoot 환경에서 hikariCP, mybatis, PostgreSQL DB를 연동해보자!


2. 환경

 - SpringBoot

 - Gradle

 - PostgreSQL

 - mybatis

 - JDK 1.8


3. hikariCP란?

  • hikari : '빛'의 일본어, CP : ConnectionPool, 뭐다? 빛처럼 빠른 Connection Pool !(?) 이라는 의도로 히카리라고 지었는지는 모르겠지만, JDBC ConnectionPool 중 하나이다. (실제로 tomcat ConnectionPool 보다 성능이 좋다)
  • ConnectionPool이란, 서버 시작 시 DB와의 Connection을 일정 개수 생성하여 Pool에 저장시켜 놓는 기술이다. 그 후 DB에 접근할 일이 생기면 Pool에서 놀고있는 Connection을 가져다 쓰고, 사용이 끝나면 다시 반환시켜 놓는다. ConnectionPool을 사용하지 않으면 트랜잭션 요청이 들어올때마다 Connection을 맺는 작업을 해야하는데, 접속이 몰리게 되면 서버에 부하를 가져올 수 있다. 그래서 일반적으로 ConnectionPool 방식을 많이 사용한다.
  • 결론은 hikariCP란 JDBC ConnectionPool이며, 스프링 부트 2 버전에서는 jdbc 의존성 주입 시 기본적으로 요녀석을 제공할 만큼 똑똑하고 빠른놈이란걸 알 수 있다.

 4. 의존성 주입

    •  build.gradle에 postgreSQL, jdbc, mybatis에 대한 의존성을 추가해주자.
//postgreSQL
runtimeOnly 'org.postgresql:postgresql'

//jdbc (hikari)
implementation 'org.springframework.boot:spring-boot-starter-jdbc'

//mybatis
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.4'

5. hikari 설정

  • hikari 설정은 크게 DB 접속 정보Connection Pool 설정으로 구성된다. application.properties 파일에 다음과 같이 추가하도록 하자.
# DB 접속정보
spring.datasource.hikari.driver-class-name=org.postgresql.Driver
spring.datasource.hikari.jdbc-url=jdbc:postgresql://[URL]:5432/[DB명]
spring.datasource.hikari.username=[접속 ID]
spring.datasource.hikari.password=[접속 PW]
spring.datasource.hikari.pool-name=[PoolName - 임의]

# CP Setting
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.maximum-pool-size=50
spring.datasource.hikari.minimum-idle=30
spring.datasource.hikari.idle-timeout=60000
  • DB 접속정보와 ConnectionPool 셋팅 정보를 입력해주었다. CP Setting 정보는 다음과 같다.
  • connection-timeout : 클라이언트가 Pool에 Connection을 요청했을 때 기다리는 시간(ms)이다. 만약 최대 50개의 Connection을 생성해둔 상태에서 50개의 요청이 한번에 들어올 경우 51번째 클라이언트는 텅텅 비어있는 Pool에서 유휴 Connection을 기다릴 수밖에 없다. 그 참을성에 대한 시간이다. 30초동안은 기다린다는 뜻이며, 만약 30초가 지날 경우 ConnectionTimeoutException이 throw된다. 이 경우 connection-timeout 값을 늘려주거나, maximum-pool-size를 늘려줘야한다.
  • maximum-pool-size : Pool에 저장할수 있는 Connection의 최대 개수이다.
  • minimum-idle : Pool에서 저장시켜야 할 Connection의 최소 개수이다. 서버 최초 기동 시 Pool에 Connection을 생성하는데 minimum-idle을 설정할 경우 maximum-pool-size만큼 생성하지 않고 minimum-idle개수만큼 생성한다. 30개는 유휴상태로 유지시키는 것이다. 만약 DB 통신 중 에러가 발생하여 한개의 Connection이 폐기되어 유휴 커넥션이 29개가 되면 이 개수를 맞추기 위해 1개의 Connection을 생성하게 된다.
  • idle-timeout : minimum-idle 개수를 넘어가게 되면 Connection을 사용하고 Pool에 반환하는게 아닌 폐기시킨다. 앞서 말했듯이 30개의 Connection만 유휴상태로 유지시키기 때문이다. 그런데 요청이 계속 들어오면 오히려 폐기하는 것보다는 유휴상태로 유지시키는 것이 효율적인 상황이다. minimum-idle 개수를 넘어간 상황에서 Connection 추가 생성 후 해당 커넥션을 일정 시간 유휴상태로 유지시키는 설정이 idle-timeout이다. 이 설정을 넣지 않으면 바로 폐기가 될까?라고 생각할 수 있지만 그것도 아니다. default 값이 60000이기 때문에 1분동안은 유지되다가 더이상 사용되지 않을 경우 폐기된다.

6. DatabaseConfig 클래스 생성

  • 의존성 주입 및 application.properties에 CP 설정을 마친 상태에서 서버를 기동하면 다음과 같은 에러가 발생한다.
    Consider the following:
    	If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
    	If you have database settings to be loaded from a particular profile you may need to activate it (the profiles oauth,prod are currently active).
     
  • H2, HSQL db에 대한 classpath를 추가해달란 것인데, 이 오류가 뜨는 이유는 앞서 설정했던 설정값들이 현재 어플리케이션에 적용이 되지 않아 default DB인 H2, HSQL로 셋팅이 되고, 실제 application.properties에는 이에 대한 설정값이 없기 때문에 발생하는 에러이다. DB 설정하는 Configuration 클래스를 만들면 해결이 된다.

 

  • DatabaseConfig.Class
package com.modu.config;


import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

@Configuration
public class DatabaseConfig {

	@Bean
	@ConfigurationProperties(prefix = "spring.datasource.hikari")
	public HikariConfig hikariConfig() {
		return new HikariConfig();
	}
	
	@Bean
	public DataSource dataSource() {
		return new HikariDataSource(hikariConfig());
	}
	
	@Bean
	public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception{
		final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
		sessionFactory.setDataSource(dataSource);
		PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
		sessionFactory.setMapperLocations(resolver.getResources("mapper/*.xml")); 	//mapper 파일 로드
		sessionFactory.setConfigLocation(resolver.getResource("mybatis-config.xml"));//mybatis-config 로드
		return sessionFactory.getObject();
	}
	
	@Bean
	public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) throws Exception{
		final SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
		return sqlSessionTemplate;
	}
}
  • ConfigurationProperties(prefix = "spring.datasource.hikari") 구문은 prefix로 시작하는 properties 값들을 추출하여 특정 객체에 바인딩 시키는 어노테이션이다. 즉, hikari 관련 설정들을 HikariConfig Bean 객체 생성 시 바인딩을 시키는 것이다.  바인딩 시 완화된 규칙이 적용되어 있어 jdbcUrl이라는 변수에 바인딩 시 jdbc-url, jdbc_url도 정상적으로 바인딩된다. 실제로 bean 객체를 Autowired 하여 debug 해보면 properties에 설정한 데이터들이 객체화되어 들어가고 있음을 알 수 있다. 나머지 값들을 HikariConfig Bean 생성 시 기본으로 주입된 값들이다.

hikariConfig

  • DataSource를 생성할 때 위에서 만든 객체를 HikariDataSource의 생성자로 주입하면 히카리 CP에 대한 Datasource 객체가 생성된다.
  • mybatis 설정파일 및 mapper 파일을 로드하는 설정에 맞게 resource 경로에 mybatis-config.xml 파일과 mapper 폴더를 생성해준다.
  • 서버를 기동하면 정상적으로 기동됨을 확인할 수 있다. 추가적으로 로그가 설정되어 있다면 debug 레벨에 다음과 같이 Connection이 생성되었다는 로그를 확인할 수 있다.

Connection 생성 완료

 

실제 DB에서 데이터를 조회해보는 것은 다음 게시글에 포스팅하도록 하겠다.

반응형
반응형

1. 개요

 - 회사 로컬 PC에 STS 최신버전 (4.12.1) 설치 후 Lombok 연동 시 에러 발생


2. 환경

 - STS 4.12.1

 - Lombok 1.18.20


3. 에러 상황

 - 개인 PC에 설치한 STS에서는 Lombok이 정상기동되었음.

 - git을 통해 clone하여 소스 로드함.


4. 조치사항

 - 구글링을 통해 다음과 같은 조치를 하였으나 안먹힘

 1) SpringToolSuite4.ini의 마지막줄에 -javaagent:lombok.jar 코드 추가 (x)

 2) SpringToolSuite4.ini의 마지막줄에 -vmargs -javaagent:lombok.jar 코드 추가 (x)

 3) SpringToolSuite4.ini의 -vmargs 구문의 제일 위에 -javaagent:lombok.jar 코드 추가 (x)

 4) SpringToolSuite4.ini의 마지막줄에 --illegal-access=warn --add-opens java.base/java.lang=ALL-UNNAMED 추가 (x)
 5) PC 재부팅, 프로젝트 clean (x)


5. JDK 16의 보안정책 강화

 - JDK 16부터 보안정책이 강화되어 최신의 STS에 Lombok 라이브러리 연동 시 에러가 발생한다고 함.

https://github.com/projectlombok/lombok/issues/2810

 

[BUG] Unhandled event loop exception in Eclipse · Issue #2810 · projectlombok/lombok

After updating Eclipse to use Java 16, building projects gives an error. Install Lombok 1.18.20 in Eclipse, either through the update site or the jar (I tried both). If you used the update site, yo...

github.com


6. 해결

 - 최신버전 멈춰! STS 4.9.0 버전으로 재설치

 - 스무스하게 동작. 역시 최신버전보단 검증된 버전으로..

 

 

반응형
반응형

1. 개요

 - Git에서 관리하던 Gradle 프로젝트를 Clone한 후 빌드 시 org.gradle.wrapper.GradleWrapperMain가 발생하였다.


2. 원인

 - org.gradle.wrapper.GradleWrapperMain 패키지 클래스는 gradle-wrapper.jar 파일 내의 패키지이다. 이 파일이 프로젝트 내에 없었고, 그로 인해 위 클래스를 찾지 못해 발생한 에러였다. 어디선가 누락이 된것이다. 

 - 기본적으로 gradle-wrapper.jar는 '프로젝트 루트 디렉토리/gradle/wrapper' 경로에 있는데, 에러가 발생했던 프로젝트에는 이 파일이 누락되어 있었다.

gradle-wrapper.jar


3. 누락은 어디서?

 - 필자의 경우 gitignore에 *.jar 가 있었고, 그로인해 누락되었다. gitignore의 default 코드에 작성되어있어 신경을 쓰지 못한 것이 화근이었다.

gitignore


4. 해결

 - gradle.wrapper.jar파일을 gradle/wrapper 경로에 넣어주면 된다. 필자의 경우 다른 프로젝트에 있는 jar파일을 가져왔지만, 아래 링크를 통해 다운받아도 무관하다.

https://mvnrepository.com/artifact/org.gradle/gradle-wrapper/5.2.1

 

 

 

반응형
반응형

1. 개요

 - Git webhook을 사용하여 배포 자동화 시스템을 구축해보자.


2. 준비

 - Jenkins

 - Git 레포지토리

 - 스프링 부트 / Gradle 프로젝트 (jar파일 배포)

 


3. webhook이란?

 - webhook이란 원격 저장소의 소스에 push, commit 등의 이벤트가 발생하면 Jenkins와 같은 CI 서버에 해당 이벤트를 전달하는 기능입니다. Jenkins에서 이 이벤트 정보를 받아 리빌드, 배포와 같은 작업을 연계하여 진행해보도록 하겠습니다.

 


4. Item 생성

 4.1. Freestyle project를 생성합니다. 

freestyle 프로젝트 생성

 

 4.2. 설명 및 GitHub project url을 입력합니다. 실제로 여기에 입력한 Git 주소가 연동에 사용되진 않습니다.

 

 4.3. 소스코드 관리로 Git을 체크한 후, 실제로 연동할 Git 주소를 입력합니다. 그럼 인증을 하라는 에러가 발생하며,  Git 계정 인증 시 'password 방식'이 지원을 하지 않으니 'Token authentication 방식'을 사용하라고 합니다. 그럼 토큰을 만들어야겠죠? 토큰은 깃허브 홈페이지에서 만들 수 있습니다.

Git 주소 연동

 

 4.4. 하던 작업을 잠깐 멈추고 '깃허브 로그인 / settings / developer settings / Personal access tokens' 탭에 들어가 Generate new token 버튼을 선택합니다.

token 생성

 

 4.5. 리포지토리에 대한 전체 접근 권한 및 hook 권한을 체크한 후, Generate Token 버튼을 눌러 토큰을 생성합니다.

  * repo, admin:repo_hook을 체크하면 됩니다.

  * Expiration을 No expiration으로 설정하면 토큰 만료 기간 없이 계속 사용 가능합니다.

token 권한 설정

 

 4.6. 생성된 토큰 값을 확인 후 복사합니다.

 * 참고로 해당 페이지를 넘어가게 되면 현재 발급된 토큰 키값을 다시 확인할 수 없으며, 분실 시 재발급 받아야합니다.

토큰 값 확인 및 복사

 

 4.7. Jenkins로 돌아가 Credentials / Add를 선택 후 Kind에는 Username with password, UserName에는 GitHub ID, Password에는 복사한 토큰 키를 입력합니다. 아래 ID와 Description은 식별자와 설명입니다. 임의로 입력 후 Add를 누릅니다. 에러가 사라졌다면 인증이 완료된 것입니다.

자격증명 등록

 

 4.8. Branch는 Master로 설정합니다.

 

 4.9. 빌드 유발은 GitHub hook trigger for GITScm polling을 선택합니다. 이 옵션이 있어야 webhook을 통해 Jenkins로 들어온 push 이벤트 정보를 인식하여 빌드를 유발할 수 있습니다. 실제 빌드는 다음 Build 탭에서 설정합니다.

빌드 유발

 

 4.10. 필자의 경우 Gradle SpringBoot 프로젝트이기때문에 Invoke Gradle Script를 선택 후 Tasks에 bootJar를 입력했습니다. build.gradle 파일에는 다음 코드를 입력하여 modu.jar라는 파일명으로 빌드되도록 설정한 상태입니다.

 * jar 파일 빌드가 완료되면 빌드 후 조치 부분에 빌드된 jar 파일을 실행시키도록 하여 스프링 부트에 내장되어 있는 tomcat 서버를 통해 서비스를 기동할 예정입니다.

build.gradle

 

Build 설정

 

 * Gradle Version에서는 빌드에 Gradle을 지정해줘야하는데 이는 Global Tool Configuration 탭에서 설정이 가능합니다.

 

 4.11. 빌드 성공 시 start.sh라는 스크립트를 실행하도록 합니다. 해당 스크립트안에는 현재 실행되고 있는 프로세스가 있다면 kill 후 재시작하는 코드가 작성되어 있습니다. Script에 해당하는 경로에 start.sh 파일을 적절히 커스터마이즈하여 생성해줍시다.

 

 * start.sh

#!/bin/bash

echo "PID Check..."
CURRENT_PID=$(ps -ef | grep java | grep modu | awk '{print $2}')

echo "Running PID: {$CURRENT_PID}"

if [ -z ${CURRENT_PID} ] ; then
        echo "Project is not running"
else
        echo "Kill Current PID"
        kill -9 $CURRENT_PID
        sleep 10
fi

echo "Deploy Project...."
nohup java -jar /var/lib/jenkins/workspace/practice-project/build/libs/modu.jar >> /home/ec2-user/practice-script/modu.log &

echo "Done"

~

 

 

 이걸로 간단한 형태의 Item 생성 및 설정이 끝났습니다. 이제 마스터 브랜치로 push를 하여 자동으로 빌드 및 jar파일을 실행시켜 배포가 되는지 확인해보도록 하겠습니다.


5. Git Push

 5.1. 소스코드 수정 후 GitHub Desktop 프로그램을 사용해 commit / push를 진행합니다.

push !


6. Jenkins 자동 빌드 및 배포 확인

 6.1. Push를 하면 자동으로 Jenkins 빌드가 시작됩니다. Build History에서 확인이 가능합니다.

자동 Build

 6.2. 해당 빌드 선택 후 Console Output을 확인하면 빌드 로그를 확인할 수 있습니다. 확인 결과, 빌드에 성공하여 BUILD SUCCESSFUL를 뿌리고 있습니다. 그에 따라 start.sh 스크립트를 실행하는 로그도 확인되고 있습니다. 스크립트도 정상적으로 실행되었으니 실제로 서비스가 올라갔는지 확인해보도록 하겠습니다.

 

Build Log

 

 6.3. public DNS로 조회하여 서비스 확인 결과, 정상적으로 jar파일이 실행되어 tomcat 서비스가 올라간 것을 확인할 수 있습니다.

서비스 정상 기동 확인


7. 마치며...

 Jenkins를 사용하여 정말 간단한 구조의 DevOps 환경을 구성해보았습니다. SpringBoot / Gradle 프로젝트, Git을 통한 소스관리, Jenkins를 통한 자동 빌드 및 배포, Swagger를 통한 REST API Interface를 제공하게 되었네요.. 뿌듯..

 아무래도 프리티어 서버를 통해 작업을 진행하다 보니 중간중간 메모리 부족으로 인한 문제도 발생하였습니다. 간혹가다 빌드 시 OutofMemory가 발생하거나, 서버 자체가 꺼져버리는 현상이었습니다. 그럴때마다 AWS EC2 인스턴스를 재시작했었는데, 현재는 Swap Memory를 0 > 2GB로 늘려주어 해결한 상태입니다. 관련 포스팅 다음에 진행하여 링크를 남겨놓도록 하겠습니다!

반응형
반응형

1. 개요

 - Amazon Linux 2 OS에 Jenkins 설치


2. 준비

 - Amazon Linux 2 서버


3. 데몬화 모듈 설치

 - Jenkins를 설치하는 방법 중 하나는 yum 명령어를 이용하는 것이다. 그런데 Amazon Linux 2 서버에서는 yum 명령어를 사용하여 설치했을 때 daemonize 에러가 발생한다.

Package: jenkins-2.308-1.1.noarch (Jenkins) Requires: daemonize

 

 - Amazon Linux 2 OS는 다른 linux os와는 다르게 daemonize 라는 모듈을 기본으로 지원하지 않는다. 때문에 daemonize를 install 해줘야한다. 다음 명령어를 통해 설치를 진행하자

vim /etc/yum.repos.d/epelfordaemonize.repo // epelfordaemonize.repo 파일 생성

-- 다음 코드 입력 후 저장 --
[daemonize]
baseurl=https://download-ib01.fedoraproject.org/pub/epel/7/x86_64/
gpgcheck=no
enabled=yes

 

 - 만약 저장 시 readonly 에러가 뜰 경우 :wq가 아닌 다음 명령어를 입력하자

:w !sudo tee % > /dev/null

 

 - 저장이 완료되면 모듈을 설치한다.

yum install daemonize -y //daemonize 설치

4. Jenkins 설치

 - 데몬화 모듈 설치가 완료되면 Jenkins를 설치한다.

 - 이왕이면 현재 서버의 Java 버전과 동일한 버전으로 Jenkins를 설치한다.

yum install jenkins java-1.8.0-openjdk-devel -y

 


5. Jenkins 실행

 - 설치가 완료되면 Jenkins를 실행한다. 만약 권한 에러가 뜨면 sudo를 붙여주자

sudo service jenkins start  //jenkins start
sudo service jenkins stop   //jenkins stop

 

 - 시작 명령어를 입력했을 때 다음과 같은 로그가 출력될 경우 정상적으로 서비스가 올라간 것이다.

jenkins start

 - 프로세스 확인 명령어로 실행 여부를 확인 가능하다. port, pid 등의 정보가 조회된다.

ps -ef | grep jenkins

프로세스 정보

 

 - 이제 실제 Jenkins 페이지로 접속해보자. 기본적으로 ip:8080 포트를 입력하면 접속된다.

시작화면

  - 만약 8080 포트가 이미 사용중이라면 포트를 변경해주어야한다. 필자의 경우 9090으로 변경 후 AWS 보안그룹에 내 IP 에 대해 9090 포트를 오픈해주었다. 포트 변경 명령어는 다음과 같다.

sudo vim /etc/sysconfig/jenkins //jenkins 설정파일

코드에서 JENKINS_PORT 값을 원하는 포트로 수정 후 저장

 

  -  수정한 포트는 Jenkins 재시작 시 적용된다.

 


6. 마치며

 - 다음은 Jenkins와 git, gradle을 연동하여 git 마스터 브랜치에 push가 갈 경우 빌드 및 배포 자동화에 대한 블로깅을 하도록 하겠다. 실제로 작업은 해놓았으나, 내용 전달을 위한 정리가 아직 되지않았다.

반응형
반응형

1. 개요

  • AWS 서버에 스프링 부트 프로젝트를 jar파일 배포한다.

2. 준비

  • Amazon Linux 2 서버
  • Spring Boot + gradle 프로젝트
  • JDK 설치
  • Git 설치 및 Source Clone

3. JDK 설치

  • 프로젝트를 배포하기 전 AWS 서버에 JDK를 설치해야합니다. SpringBoot의 build.gradle 파일의 sourceCompatibility를 확인해봅시다. sourceCompatibility는 현재 프로젝트에 호환된 Java 버전을 의미합니다.

프로젝트 java 버전 확인

 

  • 필자의 경우 java 1.8 이므로 aws서버에 openJDK 1.8 버전을 설치를 위해 다음 명령어를 입력하겠습니다.
 sudo yum install -y java-1.8.0-openjdk-devel.x86_64

 

  • 설치가 완료되었다면 인스턴스의 Java 버전을 8로 변경해야합니다. 다음 명령어를 입력 후 인스턴스에 적용할 java 버전의 번호를 입력합니다.
sudo /usr/sbin/alternatives --config java

자바 버전 선택

  • 필자의 경우 방금 설치한 java 버전만 있고, Selelction이 1로 지정되어 있으므로 입력창에 1을 입력합니다.
  • 설정이 완료되면 서버의 java 버전을 확인합니다.
java -version

자바 버전 확인

  • 설정한 Java 버전이 조회된다면 스프링 부트 jar 배포에 대한 기본적인 준비는 끝났습니다.이제 배포를 해봅시다!

4. Git 설치 및 Clone

  • 프로젝트 파일이나 빌드된 파일을 AWS 서버로 전송 해야합니다. 이 과정에 대해 채택할 수 있는 방법은 FTP가 될수도 있지만 필자의 경우 Git Clone을 통해 프로젝트 파일을 AWS 서버에서 받은 후 서버 내에서 빌드를 하는 방식을 채택했습니다.
  • 다음 명령어를 입력해 AWS 서버에 git을 설치 및 확인합니다.
sudo yum install git // git 설치
git --version //git 설치(버전) 확인

git 설치버전 확인

  • 설치가 완료되면 clone 될 디렉토리를 생성한다. 필자의 경우 홈 디렉토리 기준으로 "app" 폴더를 생성하여 그 안에 git에서 clone한 소스를 넣겠습니다.
mkdir ~/app  //app 폴더생성

 

  • 폴더 생성이 완료되면 본인의 깃허브 웹페이지에서 주소를 복사합니다.

Clone할 url 복사

  • AWS 서버에 다음 명령을 입력하여 clone을 진행합니다.
git clone 복사한 주소

 

  • clone이 정상적으로 완료되면 app 폴더 안에 프로젝트 소스들이 들어갔을겁니다. 이제 배포를 진행할 차례입니다.

5. 배포

  • 프로젝트 경로로 들어가 다음 명령어를 입력해 jar 빌드합니다.
./gradlew bootjar

 

  • build가 완료되면 자동적으로 build/libs 폴더 내에 "프로젝트 명/버전.jar" 형식으로 파일이 생성됩니다. 배포는 간단합니다. 이 파일을 실행시켜주면 됩니다. 스프링 부트는 jar 파일 빌드 시 tomcat 서버가 내포된 형태로 빌드되기 때문에 별도의 tomcat 서버가 필요없습니다.
java -jar jar파일 명

jar 파일 실행

  • 배포가 완료되었습니다. 배포된 서버로 접속하기 위해 '퍼블릭 DNS:포트'를 입력해봅시다.
  • 퍼블릭 DNS는 AWS 인스턴스 상세정보로 들어가면 확인할 수 있으며 포트는 배포 로그의 마지막 부분에서 확인할 수 있습니다.

퍼블릭 DNS

 

  • 짜잔~ 다음과 같이 정상적으로 접속이 되었습니다.

배포된 어플리케이션 접속 성공 화면

 

  • 만약 접속이 되지 않으신다면 AWS내에 설정한 보안 그룹에 해당 포트가 오픈되어있는지 확인해봅시다. 필자의 경우 해당 포트가 열려있지 않아 최초에 접속이 되지 않았습니다.
  • 인스턴스 세부정보 > 보안 > 인바운드 규칙에 8090포트를 추가해줍시다.
    인바운드 규칙 추가

6. 마치며...

  • Git 연동 및 클론, 빌드, AWS 배포를 해보았습니다. 되게 간소하게(?) 진행했지만 AWS 서버에 배포를 해본건 처음이라 감회가 새로웠다. 다음 목표는 Jenkins를 연동하여 git commit이 있을 때 자동으로 AWS 서버에 수정된 코드가 배포되도록 하는 것이다. 이 작업이 끝나면 본격적인 서버 로직을 작업할것이다
반응형
반응형

1. 개요

 - 프리티어 계정으로 EC2 서버를 생성한다.

 - 생성된 EC2 서버에 접속한다.


2. 환경

 - Window 10

 - AWS 계정 생성 완료


3. EC2 란?

 - Elastic Compute Cloud의 약자로 AWS에서 제공하는 사양, 용량 등을 유동적으로 사용 가능한 서버를 말합니다.

 - 프리티어 계정은 이 중 t2.micro(CPU 1, 메모리 1GB, 용량 최대 30GB)라는 서버를 1년동안 무료로 사용 가능합니다. 

 - 프리티어 기준으로 월 750 시간의 제한이 있으며 초과하면 과금이 발생합니다. 하지만 24*31은 744 시간이므로 한대의 프리티어 서버를 운용한다면 과금은 발생하지 않습니다.


4. EC2 t2.micro 서버 생성

 4.1. 검색창에 ec2 검색 및 선택

ec2 검색

 

 4.2. 인스턴스 시작 버튼 선택

  - 화면 진입 시 상단에 위치한 인스턴스 시작 버튼을 선택합니다.

인스턴스 시작

 

 4.3. OS 선택

  - 프리티어로 사용 가능한 Amazon Linux 2 AMI 64비트를 선택합니다.

 

 4.4. EC2 유형 선택

  - 프리티어로 사용 가능한 t2.micro 를 선택합니다.

 

 4.5. 인스턴스 세부 정보 구성

  - 이 부분에서 딱히 설정해야할 부분은 없습니다. 넘어갑시다.

 

 4.6. 스토리지 설정

  - t2.micro 에서 지원 가능한 최대 스토리지 용량인 30GB를 입력합니다.

 

 4.7. 태그 추가

  - Name 이라는 Key를 생성하고 값(ex. 프로젝트 명)을 입력해줍니다.

 

 4.8. 보안 그룹 추가

 - [SSH, 내 IP] : SSH를 통해 서버에 접근할 수 있도록 추가합니다. '나'만 접근 가능하도록 하기 위해 '내 IP'로 설정하였습니다. 만약, 서버에 접근해야할 동료가 있다면 SSH 유형을 하나 더 만들어 추가해줍시다.

 - [사용자 지정 TCP, 8080] : 8080 포트로 모든 IP가 접근 가능하도록 설정하였습니다.

 - [HTTPS, 443] : 443 포트로 모든 IP가 접근 가능하도록 설정하였습니다.

 

 4.9. 검토

  - 다음 항목을 검토 후 이상이 없다면 시작버튼을 클릭합니다.

   - OS : Amazon Linux 2

   - 인스턴스 유형 : t2.micro

   - 보안그룹

   - 스토리지 : 30GB

 

 4.10 키페어 설정

 - 키 페어 설정을 하게 되면 서버 접근 시 접근자의 개인키와 서버의 공개키를 체크합니다. 보안그룹을 설정했지만 키페어 설정도 꼭 필요합니다.

  - 현재 보안 그룹 설정을 했기 때문에 마스터 서버로의 접근은 '내 IP'에서만 가능합니다. 악의적인 사용자가 '나'의 서버 접근 정보를 알고있다고 하더라도 IP가 달라 접근이 불가능합니다. 하지만 극단적인 예로, 내 옆집에 사는 사람이라면 제 공인 IP로 접근이 가능할 수 있고, 그렇게 되면 제 서버로의 접근도 가능할 수 있습니다. 만약, 사내 프로젝트를 위해 EC2 서버를 생성하고 보안그룹을 '내 IP'로 설정하였습니다. 그렇게되면 같은 사내망을 사용하는 모든 사람들이 접근 가능하게 됩니다.

  - 이런 유형의 접근을 막기 위해 필요한 것이 바로 키페어입니다. 키페어를 설정하면 서버의 공개키와 대응되는 개인키를 발급받게 되고, 실제 접근 시 이 키파일을 서버로 제공해야합니다.

 - RSA 유형으로 키 페어 생성 선택 후 키 페어 이름에 프로젝트 명을 넣습니다. 그 후 키 페어 다운로드를 클릭합니다.

 - pem 확장자의 개인키 파일 다운로드가 확인되면 인스턴스 시작 버튼을 클릭합니다.

 

 4.11. EC2 인스턴스 생성 완료

  - 다음과 같은 화면이 나올 경우 EC2 인스턴스가 생성 완료된것입니다.

  - 이제 개인키 파일을 통해 내가 만든 EC2 서버에 접속해볼 차례입니다.


5. EC2 서버 접속

 - 서버에 접근하기 위해서는 몇가지 과정이 필요합니다. 차근차근 밟아봅시다.

 

 5.1. putty, puttygen 설치

  - putty : SSH를 통한 서버 접속에 사용합니다.

    * 설치 url : https://putty.softonic.kr/

 

PuTTY

완전하고 안정적인 텔넷 및 SSH 클라이언트

putty.softonic.kr

  - puttygen : 서버로 접근하기 위해서는 개인키 파일(.pem)을 .ppk 확장자로 변환해야 하며, 이 변환에 사용합니다. 

    * 설치 url : https://www.puttygen.com/download-putty

 

 5.2. puttygen 실행

  - puttygen.exe 실행 후 Conversions > import key 를 선택합니다.

import key

 5.3. 개인키 파일 임포트

  - 아까 발급받은 개인키 파일을 선택합니다.

pem 파일 선택

 

 5.4. ppk 파일 생성

  - 다음과 같은 화면이 조회되면 Save private Key 버튼을 선택하여 ppk 파일을 생성합니다.

  - 알림 메시지가 하나 뜨는데 무시하고 예를 누릅니다.

  - 파일이 생성되었다면 이제 서버로 접속할 차례입니다.

ppk 파일 생성

 5.5. putty 실행

  - putty를 실행한 후 HostName에 ec2-user@[퍼블릭 DNS]를 입력합니다.

  - ec2-user는 Amazon Linux 2 생성 시 자동 생성되는 ID 입니다. 다른 OS를 선택했다면 헤매지 말고 구글링!

HostName 입력

  

  - 만약, 퍼블릭 DNS를 모르겠다면 EC2 콘솔에 접속해 인스턴스 상세보기로 확인할 수 있습니다.

 

 5.6. Auth 설정

  - putty 카테고리의 Connection > SSH > Auth를 선택 후 Browse 버튼을 선택하여 생성한 ppk 파일을 로드합니다.

Auth 설정

 5.7. Open!

  - 모든 설정이 끝났습니다. 이제 Open 버튼을 눌러 서버에 접속해봅시다!

  - 다음과 같은 화면이 뜬다면 성공입니다!

  - 만약 에러가 뜨신다면 다음을 확인해보세요.

    1) 서버에 대한 개인키가 맞는지

    2) OS에 대한 ID (ex. amazon linux = ec2-user)가 맞는지

    3) HOST NAME에 퍼블릭 DNS가 정확하게 입력됐는지

  - 저의 경우 2번과 3번이 잘못되어 시간이 걸렸답니다 ㅠㅠ.


6. 마치며

 - 예전에 책을 보며 무작정 따라한 적이 있는데, 이게 무슨 설정이고, 왜 하는지도 이해가 가지 않았었던것 같아요. 용어의 의미와 역할을 이해하려고 노력하며 다시 책을 따라해보세요! 훨씬 의미있고 기억에 남을거에요!

반응형

+ Recent posts