클라우드 아이디 생성

우선 사용하기 위해 클라우드 아이디를 생성해야 한다.

 

기존 네이버 아이디와 연동이 되기때문에, 해당 가입 과정은 넘어가겠다.

 

 

아래의 네이버로 간편 로그인을 사용하면 된다.

 

이제 로그인시 결제수단을 등록하면 3개월동안 사용할 수 있는 10만원치의 크레딧과 Micro Server 1년 무료 사용가능한 상태가 된다.

 

 

결제수단을 등록한 후 우측 상단의 콘솔을 누른다.

 

누르면 다음과 같은 페이지가 나오며, 사용중인 서비스의 사용량과 여러가지를 볼 수 있는 대시보드를 볼 수 있다.

 

 

좌측 바의 Service 카테고리를 클릭하면 수 많은 지원 서비스에 대해 나오며, 우리는 Compute 카테고리에 있는 Server 서비스를 클릭한다.

 

All 글씨 아래의 Server를 클릭하면 아래 화면이 나오게 된다.

 

서버가 하나도 존재하지 않는다면 서버 상품에 대한 표가 나오며, 왼쪽에 있는 서버 생성 버튼을 눌러주도록하자.

 

그러면 해당 화면이 나타나며, 위의 검정색 부분을 보면 서버 운영체제 베이스이미지를 고르게 되어있는데, 나는 Ubuntu 기반 Linux 서버를 운영할 것이며, 현재 일자 기준으로 문의한 결과 micro 서버는 CenOS와 Ubuntu 20, Rocky운영체제에서만 만들 수 있게 되어있다. 따라서 "최신 서버 이미지"가 아닌 "NCP 서버 이미지"로 들어가보자

 

 

넘어오면 여러가지 운영체제가 존재하는데, 필자는 ubuntu 20.04-base 이미지를 선택했다.

 

고르고 난 후 VPC와 서브넷을 설정하면 서버 스펙을 결정할 수 있다.

그러면 위 사진과 같이 나타나는데 해당 Micro 서버를 택하고 나머지 옵션등을 세팅하면 끝이다.

 

 

모두 세팅하면 두번째인 스토리지 설정단계로 넘어간다.

 

별건 없고 그냥 스토리지 용량만 정의해주면 된다. 필자는 기본인 10GB를 그대로 사용했다.

 

인증키 파일 생성 과정이다. 이름에 네이밍 룰은 없으며 관리자가 찾기 편한 이름으로 하는 것이 좋다. 추후에 서버 접속 패스워드 확인 등 사용할 곳이 많으므로 이름을 잘 정의하도록 하자.

 

 

네트워크 접근 설정

네트워크 접근 설정은 서버에 접속할 수 있는 인바운드, 아웃바운드에 대한 룰을 정의해놓고 어떤 룰을 적용할지 선택하면 된다. 난 그냥 기본옵션으로 했으며, 모든 IP의 아웃바운드를 받게끔해놨다.

 

그러면 최종 설정에 대한 확인절차를 가지며, 본인이 설명한 것이 맞다면 확인버튼을 누르도록 하자.

 

이후 다시 Service -> Compute -> Server에 보면 생성한 서버의 상태가 "생성 중" 으로 나타나 있는 것을 볼 수 있으며, 해당 상태가 "운영 중"으로 변경되면 그때부터 서버가 작동하고 있다고 판단할 수 있다.

 

그러면 서버에 접속하기 위해 생성되는 동안 Putty를 설치하자. 설치하는 과정은 구글에 Putty를 검색하고 다운로드만 받으면 되기때문에 생략하겠다.

 

설치 후 위 사진처럼 서버가 운영중이라면, 우선 해당 서버의 정보를 확인하기 위해 서버 row 클릭하자. 그러면 아래와 같이 공인 IP등 정보가 나타난다.

 

이후 Putty를 실행하면 아래 사진이 나오는데, 위 사진의 공인 IP와 기본으로 열리는 FTP 포트인 22번 포트를 입력하자.

그러면 아래와 같은 화면으로 연결된다.

로그인 화면

해당 OS에 로그인을 해야하는데, 이것을 확인하기 위해 서버생성시 설정했던 인증키 파일이 필요하다.

 

우선 서버 목록 화면 상단의 "서버 관리 및 설정"을 클릭하고, "관리자 비밀번호 확인"을 눌러준다.

 

 

이후 아래 화면에서 생성됬던 인증키 파일을 올리고 확인을 누른다.

그러면 아래와 같은 화면에 ID와 비밀번호가 나올것이다.

 

확인 후, 첫 화면에 관리자 이름을 입력한다.

 

패스워드는 Git 처럼 입력해도 아무것도 보이지 않으니 당황하지말고 천천히 입력하자.

 

제대로 입력되면 위와같이 성공적으로 접속되었다고 나타나며, 해당 서버에서 작업을 진행하면 되겠다.

 

서버 띄우는데도 한참동안 걸렸다. 우선 우분투 22에서도 만들었다는 글을 보고 22에서 뻘짓을 3시간정도 하다가 문의에 대한 답변을 보고 그제야 알아챘다. 하지만 앞으로 도커로 이미지를 배포하고 쿠버네티스를 또 배워서 해당 클라우드에 적용하게 되는것에 대한 압박감이 있긴하다. 첫걸음이 어렵다고 클라우드서버 생성은 했으니 클라우드 지식들을 학습하며 다들 같이 나아가길 바라고 있다.

 

 

Stream이란?

Stream은 원문 그대로 해석시 “흐르다” 라는 뜻을 가지고 있습니다. 기술의 개발로 인해 서비스가 고도화되고 복잡해지면서 데이터 처리에 대해 효율적인 방안이 필요했고, 그러다 나온 것이 이 Stream입니다.

 

Stream은 Oracle 공식문서에서 “연속된 데이터를 효율적으로 처리하기 위한 인터페이스” 라고 나와있습니다.이제부터 그 이유를 알아보겠습니다.

 

우선 코드로 어떤 점이 차이가 나는지 알아보겠습니다.

 

우선 컬렉션을 하나 만들어 두겠습니다.

다음 값을 출력하면 해당 리스트가 생겼습니다.

여기서 저는 “구” 라는 성을 가진 데이터만 뽑아서 리스트를 가져오고 싶다는 요청을 받았다고 가정하고, 우선 1번째 글자가 구씨인 데이터만 골라서 다른 리스트를 가지도록 해보겠습니다.

public class Main {
    public static void main(String[] args) {
        List<String> humenName = new ArrayList<>();
        humenName.add("구길동");
        humenName.add("홍길동");
        humenName.add("김길동");

        System.out.println(humenName);
        List<String> kooList = new ArrayList<>();

        for (int i = 0; i < humenName.size(); i++) {
            String name = humenName.get(i);
            String nameSlice = name.substring(0,1);
            if (nameSlice.equals("구")) {
                kooList.add(name);
            }
        }

        System.out.println(kooList);
    }
}

이렇게 간단한 주제임에도 반복문이 5줄이나 나오며, 쓸모없이 하나의 변수가 더 나오게 되어버렸습니다.

그렇다면 이것을 스트림과 람다식 문법으로 사용하면 어떻게 변할까요?

List<String> kooList = humenName.stream()
                .filter(item -> item.substring(0,1).equals("구"))
                .collect(Collectors.toList());

보시면 머 쓸데없는 코드를 제외하더라도 코드의 순수한 양은 물론이고, 가독성도 좋아진 것을 한눈에 보실 수 있습니다.

이렇게 코드를 먼저 비교해봤는데요, 코드의 관리나 개발측면에서도 스트림을 사용하면 좋을 거같아 현재 신규 프로젝트에도 사용해보고 있습니다.

Stream의 특징

이제 코드로 사용해야하는 간단한 이유를 먼저 알아봤으니 Stream의 특징과 그에 따라오는 장점을 알아볼 차례입니다.

  1. 파이프 라이닝
  • 위의 코드를 보시면 데이터를 스트림화 하고 필터링 후에 콜렉션화 하는 과정이 이어져 있으며, 그에 따라 자연스럽게 파이프라인이 잡혀있는 것을 볼 수 있습니다.
  • 이렇게 스트림 연산은 연산끼리 연결해서 스트림 자신을 반환하는데, 여기에서 laziness(게으름)과 short-circuting(쇼트서킷)이라는 장점을 얻을 수 있습니다.
    • laziness(지연) : Stream은 코드가 읽히는 즉시 연산을 하지 않고 최소한의 준비작업만 실시하다가, 해당 결과값에 대한 호출이 있을 때 계산한다.
    • loop Fusion(반복병합) : 연속적으로 체이닝된 복수의 연산을 하나로 병합시킨다.
    • short-circuting(쇼트 서킷) : 불필요하다 판단된 연산의 수행을 하지않고 중단한다.
  1. 내부 반복
  • Stream은 Collection과 반복문을 명시하지않아도 내부에서 순환시킬 수 있다.

이렇게 여러가지 장점에 대해 찾아보았습니다. 물론 Stream이 만능은 아닐 것입니다.

기본적으로 호출 시점이 확실하고 용량이 큰 BLOB데이터 같은 경우에는 비동기로 돌려서 미리 호출에 대비해야하지않나 라는 아주 얕은 생각이 내비치기도 하였으며,

파이프라인을 DB에 쿼리문 튜닝으로 1차적으로 잡아야 좀 더 효율적으로 움직일 수 있을거라는 생각도 조금은 들었습니다.

다음번에 Stream의 여러 종류중 하나에 대해 다루게 된다면 이 이론들을 다시 곱씹어 보겠습니다.

Stream이란?

Stream은 원문 그대로 해석시 “흐르다” 라는 뜻을 가지고 있습니다. 기술의 개발로 인해 서비스가 고도화되고 복잡해지면서 데이터 처리에 대해 효율적인 방안이 필요했고, 그러다 나온 것이 이 Stream입니다.

Stream은 Oracle 공식문서에서 “연속된 데이터를 효율적으로 처리하기 위한 인터페이스” 라고 나와있습니다.이제부터 그 이유를 알아보겠습니다.

우선 코드로 어떤 점이 차이가 나는지 알아보겠습니다.

우선 컬렉션을 하나 만들어 두겠습니다.

다음 값을 출력하면 해당 리스트가 생겼습니다.

여기서 저는 “구” 라는 성을 가진 데이터만 뽑아서 리스트를 가져오고 싶다는 요청을 받았다고 가정하고, 우선 1번째 글자가 구씨인 데이터만 골라서 다른 리스트를 가지도록 해보겠습니다.

public class Main {
    public static void main(String[] args) {
        List<String> humenName = new ArrayList<>();
        humenName.add("구길동");
        humenName.add("홍길동");
        humenName.add("김길동");

        System.out.println(humenName);
        List<String> kooList = new ArrayList<>();

        for (int i = 0; i < humenName.size(); i++) {
            String name = humenName.get(i);
            String nameSlice = name.substring(0,1);
            if (nameSlice.equals("구")) {
                kooList.add(name);
            }
        }

        System.out.println(kooList);
    }
}

이렇게 간단한 주제임에도 반복문이 5줄이나 나오며, 쓸모없이 하나의 변수가 더 나오게 되어버렸습니다.

그렇다면 이것을 스트림과 람다식 문법으로 사용하면 어떻게 변할까요?

List<String> kooList = humenName.stream()
                .filter(item -> item.substring(0,1).equals("구"))
                .collect(Collectors.toList());

보시면 머 쓸데없는 코드를 제외하더라도 코드의 순수한 양은 물론이고, 가독성도 좋아진 것을 한눈에 보실 수 있습니다.

이렇게 코드를 먼저 비교해봤는데요, 코드의 관리나 개발측면에서도 스트림을 사용하면 좋을 거같아 현재 신규 프로젝트에도 사용해보고 있습니다.

Stream의 특징

이제 코드로 사용해야하는 간단한 이유를 먼저 알아봤으니 Stream의 특징과 그에 따라오는 장점을 알아볼 차례입니다.

  1. 파이프 라이닝
  • 위의 코드를 보시면 데이터를 스트림화 하고 필터링 후에 콜렉션화 하는 과정이 이어져 있으며, 그에 따라 자연스럽게 파이프라인이 잡혀있는 것을 볼 수 있습니다.
  • 이렇게 스트림 연산은 연산끼리 연결해서 스트림 자신을 반환하는데, 여기에서 laziness(게으름)과 short-circuting(쇼트서킷)이라는 장점을 얻을 수 있습니다.
    • laziness(지연) : Stream은 코드가 읽히는 즉시 연산을 하지 않고 최소한의 준비작업만 실시하다가, 해당 결과값에 대한 호출이 있을 때 계산한다.
    • loop Fusion(반복병합) : 연속적으로 체이닝된 복수의 연산을 하나로 병합시킨다.
    • short-circuting(쇼트 서킷) : 불필요하다 판단된 연산의 수행을 하지않고 중단한다.
  1. 내부 반복
  • Stream은 Collection과 반복문을 명시하지않아도 내부에서 순환시킬 수 있다.

이렇게 여러가지 장점에 대해 찾아보았습니다. 물론 Stream이 만능은 아닐 것입니다.

기본적으로 호출 시점이 확실하고 용량이 큰 BLOB데이터 같은 경우에는 비동기로 돌려서 미리 호출에 대비해야하지않나 라는 아주 얕은 생각이 내비치기도 하였으며,

파이프라인을 DB에 쿼리문 튜닝으로 1차적으로 잡아야 좀 더 효율적으로 움직일 수 있을거라는 생각도 조금은 들었습니다.

다음번에 Stream의 여러 종류중 하나에 대해 다루게 된다면 이 이론들을 다시 곱씹어 보겠습니다.

PrintWriter와 FileWriter??

릴리즈 버젼으로 빌드한 앱은 디버깅하기 굉장히 까다롭습니다. 물론 실 기기와 연동되면 로그캣이나 여러 툴을 사용해 디버깅이 가능하지만, 고객사가 굉장히 다양한 현재 회사 특성상 이상현상 하나때문에 매번 부산, 전라도부터 전국적인 지역을 출장갈 수는 없고, 폐쇄적인 네트워크 환경상 사무실에서는 로그인도 안됩니다.

그래서 앱을 테스트할 때는 VM(ex : Nox, LDplayer)등으로 사용하는데, 이때는 로그파일에 스택트레이스등을 남겨 테스트하곤 합니다.

PrintWriter 클래스는 뭘까?

PrintWriter 클래스는 PrintStream이라는 클래스의 기능을 구현한 Writer 클래스의 확장 클래스로, 기존 Stream처럼 글자마다 flush하는 것이 아니라, printf(),println()등 PrintStream메서드가 호출될때마다 flush되어 기록되며, Stream 방식으로 구현되는 것이 아니기때문에 출력 메서드를 사용할 때 byte가 아닌 String값을 통해 기록한다.

개인적으로 해당 값에 따라 String 값을 byte로 바꾸어 넣어야하는 번거로움과 해당 코드를 줄일 수 있을 뿐 아니라, String 값을 직접 입력하기에 소스를 처음 보는 상황이 생겨도 비교적 직관성을 향상시킨다고 생각한다.

FileWriter 클래스란?

문자 파일 작성을 위한 편의 클래스로, FileOutputStream 클래스와 OutputStreamWriter클래스의 기능을 간소화 시킨 것이다.

생성자로는 File 객체가 인자값으로 필요하며, 파일명의 문자열값(String fileName)이 들어가도 되기에 편한대로사용하면 되겠다.

두 클래스를 같이 사용하는 이유는?

해당 목표는 apk를 디버깅할 때 txt파일에 로그를 남겨 비교적 편리한 디버깅을 하는것이다.

또한 PrintWriter생성자에 FileWriter를 인자로 넣음으로서 파일을 지정하고 쓰기작업을 하는데 굉장히 간편하게 할 수 있다.

공식 문서에 보면 PrintWriter생성자에 Writer 객체와 자동 플러시의 여부를 지정하여 File에 자동으로 PrintStream의 메서드를 사용할 시 해당 문자열 값을 지정한 파일에 기록할 수 있다.

위 사진에 나오는 메서드들이 PrintWriter에 구현되어있는 print메서드들이다.

예시 코드는 아래와 같다.

public class FileLogTest{	
	public static void main(String[] args) {
					private PrintWriter writer;
	        try {
		        writer = new PrintWriter(new FileWriter(fileName, true), true);
		        writer.println("아");
		        writer.printf("%s 하자\\n","야근");
		        writer.close();
	        } catch (IOException e) {
	            e.printStackTrace();
	        }
	    }
}

광범위 하게 사용한다면 try-catch를 해당 로직에 감싸서 편하게 writer.println(String message)등의 방식으로 입력하면 된다. 하지만 정말 제한적인 환경에서 도저히 사용할 방법이 없을때 하는 로깅방법이라 크게 자세히 다루진않고, 다른 예제나 기능과 관련하여 나오게 되면 조금 더 딥하게 알아보고싶다.

PrintWriter와 FileWriter??

릴리즈 버젼으로 빌드한 앱은 디버깅하기 굉장히 까다롭습니다. 물론 실 기기와 연동되면 로그캣이나 여러 툴을 사용해 디버깅이 가능하지만, 고객사가 굉장히 다양한 현재 회사 특성상 이상현상 하나때문에 매번 부산, 전라도부터 전국적인 지역을 출장갈 수는 없고, 폐쇄적인 네트워크 환경상 사무실에서는 로그인도 안됩니다.

그래서 앱을 테스트할 때는 VM(ex : Nox, LDplayer)등으로 사용하는데, 이때는 로그파일에 스택트레이스등을 남겨 테스트하곤 합니다.

PrintWriter 클래스는 뭘까?

PrintWriter 클래스는 PrintStream이라는 클래스의 기능을 구현한 Writer 클래스의 확장 클래스로, 기존 Stream처럼 글자마다 flush하는 것이 아니라, printf(),println()등 PrintStream메서드가 호출될때마다 flush되어 기록되며, Stream 방식으로 구현되는 것이 아니기때문에 출력 메서드를 사용할 때 byte가 아닌 String값을 통해 기록한다.

개인적으로 해당 값에 따라 String 값을 byte로 바꾸어 넣어야하는 번거로움과 해당 코드를 줄일 수 있을 뿐 아니라, String 값을 직접 입력하기에 소스를 처음 보는 상황이 생겨도 비교적 직관성을 향상시킨다고 생각한다.

FileWriter 클래스란?

문자 파일 작성을 위한 편의 클래스로, FileOutputStream 클래스와 OutputStreamWriter클래스의 기능을 간소화 시킨 것이다.

생성자로는 File 객체가 인자값으로 필요하며, 파일명의 문자열값(String fileName)이 들어가도 되기에 편한대로사용하면 되겠다.

두 클래스를 같이 사용하는 이유는?

해당 목표는 apk를 디버깅할 때 txt파일에 로그를 남겨 비교적 편리한 디버깅을 하는것이다.

또한 PrintWriter생성자에 FileWriter를 인자로 넣음으로서 파일을 지정하고 쓰기작업을 하는데 굉장히 간편하게 할 수 있다.

공식 문서에 보면 PrintWriter생성자에 Writer 객체와 자동 플러시의 여부를 지정하여 File에 자동으로 PrintStream의 메서드를 사용할 시 해당 문자열 값을 지정한 파일에 기록할 수 있다.

위 사진에 나오는 메서드들이 PrintWriter에 구현되어있는 print메서드들이다.

예시 코드는 아래와 같다.

public class FileLogTest{	
	public static void main(String[] args) {
					private PrintWriter writer;
	        try {
		        writer = new PrintWriter(new FileWriter(fileName, true), true);
		        writer.println("아");
		        writer.printf("%s 하자\\n","야근");
		        writer.close();
	        } catch (IOException e) {
	            e.printStackTrace();
	        }
	    }
}

광범위 하게 사용한다면 try-catch를 해당 로직에 감싸서 편하게 writer.println(String message)등의 방식으로 입력하면 된다. 하지만 정말 제한적인 환경에서 도저히 사용할 방법이 없을때 하는 로깅방법이라 크게 자세히 다루진않고, 다른 예제나 기능과 관련하여 나오게 되면 조금 더 딥하게 알아보고싶다.

Timer / TimerTask 클래스

Timer클래스란?

말 그대로 정말 타이머(Timer)의 역할을 수행하는 클래스이다.

schedule이라는 내부 메서드를 통해 특성시간, 기간마다 인자로 받은 TimerTask인스턴스의 run메서드 로직을 수행하여 특정 시간마다 반복적인 로직을 실행하게끔하거나 하는 식으로 원하는 로직을 수행시킬 수 있다.

TimerTask 클래스란?

Timer클래스의 schedule함수로 작업 스케줄링을 관리할 때 들어가는 추상 클래스로,

Runnable클래스에서 run함수만 따와 별도의 스레드로 함수를 구동시키는 원리인 것같다.

사용 예제

우선 Timer클래스에 들어가면 생성자부터 schedule함수의 재정의에 따라 각각의 로직을 가지고있는데, 그것은 생략하겠다.

해당 예제에서 다루는 것은 Timer클래스의 생성, TimerTask의 정의, 이후 schedule에 함수에 들어간 TimerTask의 정상 작동 확인에 대해 다루겠다.

예제 1

ackage org.example;

import java.util.Timer;
import java.util.TimerTask;

//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
public class Main {
    public static void main(String[] args) {

        Timer timer = new Timer(); // 타이머 객체생성
        TimerTask timerTask = new TimerTask() { 
        // 추상 클래스이므로 인스턴스 생성시에 가진 메서드를 구현해야함 
            @Override
            public void run() {
                System.out.println("시간이 지났습니다~"); // 작동 로직
                timer.cancel(); // 타이머 종료
            }
        };
        
        // 구동 후 5초 뒤에 timerTask의 로직을 구동한다.
        timer.schedule(timerTask,5000); 
    }
}

아주 간단한 예제로 앱을 실행하고 5초뒤에 프린팅이 찍히게끔 되어있다.

TimerTask의 로직에 timer를 종료하는 cancel()를 사용하여 한번만 구동하게끔해놓았다.

왼쪽의 실행 시간을 보면 5초 후 구동이 완료되었고, 이후에 추가로 작동하지않고 종료됨을 알 수 있다.

그럼 타이머는 객체생성부터 작동될까, schedule()부터 구동될까?

객체 생성시부터 시간이 계산되는 것 같다. Timer의 클래스를 들어가보면

    public Timer() {
        this("Timer-" + serialNumber());
    }

    public Timer(String name) {
        thread.setName(name);
        thread.start();
    }

이렇게 두개의 생성자가 있는데, 위 코드에서 기본생성자로 생성시 아래있는 타이머 생성자가 실행되면서 스레드가 시작됨으로 봤을때 생성자체가 실행된다고 . 볼수 있다.

특정 로직에만 실행시키기

그러면 특정 로직을 탈 때에 특정시간마다 실행을 시키려면, TimerTask만 미리 정의하거나 임의의 커스텀 클래스로 제작하여 빼고, 특정 로직을 탈때 타이머 인스턴스를 생성하면 되겠다.

아래는 해당 예제에 대한 코드이다.

우선 TimerTask를 커스터마이징했다.

public class CustomTimerTask extends TimerTask {
    Timer timer;

    public CustomTimerTask(Timer timer){
        this.timer = timer;
    }

    @Override
    public void run() {
        System.out.println("타이머 로직이 실행됩니다.");
        timer.cancel();
    }
}
public class Main {
    public static void main(String[] args) {
        // 특정 로직을 실행하는지에 대한 bool 값
        boolean isTimerExecute = false;

        if (isTimerExecute) {
            Timer timer = new Timer();
            TimerTask timerTask = new CustomTimerTask(timer);
            timer.schedule(timerTask,5000);
        }else{
            System.out.println("타이머를 타야하는 로직이 아닙니다.");
        }

        
    }
}

이렇게 한 후 실행해보겠다.

특정 로직이 아니기에 불과 224ms만에 실행되었고, 그대로 앱은 종료되었다. isTimerExecute 변수값을 true로 바꾼 후, 타이머 로직이 실행되는지 테스트해보자

기존처럼 5초뒤에 TimerTask에 실행이 한번 되었고, 추가 작동없이 타이머가 종료됨을 알 수 있다.

이렇게 Timer클래스와 TimerTask에 대해 알아보았다. 해당 클래스에 대한 역할은 Spring Framework같은 경우 스프링 스케줄러나 스프링 배치등 여러 대체 구현 기능이 되어있지만, 간단한 경우에는 해당 클래스로 커스터마이징을 해서 사용해도될 거 같다.

 

[Spring boot] 다중 DB(Multiple DB) 연결 세팅


프로젝트를 시작하며 업무에서 2개의 DB를 하나의 서버 프로젝트에 커넥션을 잡아줘야 하는 일을 맡게됬습니다.

항상 토이프로젝트를 하든 팀 프로젝트를 하든 하나의 DB를 가지고 세팅을 했기 때문에 구글링도 여러번 해보고 헷갈렸는데요. 혹시나 까먹고 두번 다시 이걸로 고생하고싶지 않아서 정리해두려고합니다. 

 

우선 순서를 정리해보면

 

1. 메인 DB와 서브 DB 정하기

 

2. build-gradle에 connector 추가

-- log4jdbc는 추가 설정파일(.properties) 추가

 

3. 각 DB의 config 클래스 생성 후 DB 정보 입력

 

3-1. 메인 DB와 서브 DB에 따른 설정 추가

 

4. mapper에 각 DB SQL문 작성

 

5. DAO에 메인,서브DB Bean정상 호출 확인

 

6. 웹 실행을 통한 최후 테스트

 

 

이런식으로 갑니다.

 

우선 메인 DB와 서브 DB를 정하는 이유는 잠시 뒤 보실 설정부분과 호출부분에서 Primary로 설정하는 DB는 설정이 간단하고 호출도 1개의 DB만 사용할 때 그대로 사용하면 되기때문에 자주 사용하시는 DB를 메인 DB로 정하시고 가시는게 설정시 매우 편합니다.

 

이제 시작해보겠습니다.

 

 

1. 메인/서브 DB 정하기

우선 저는 프로젝트에서 MySQL과 Tibero를 사용하기로 했는데, 자주 사용되는 DB가 Tibero이기 때문에 메인 DB는 Tibero, 서브 DB는 MySQL로 사용하겠습니다.


2. 라이브러리에 connector 추가 및 log4jdbc 설정 파일 추가


  - 우선 build.gradle의 dependencies에 필요한 DB커넥터들을 추가 후 빌드합니다.

log4jdbc쓰시는 분들은 같이 ㄱㄱ~
외부 라이브러리에서 확인하기!

  - log4jdbc.log4j2.properties 파일 생성

- src/main/resources 아래에 log4jdbc.log4j2.properties라는 파일을 생성 후 아래와 같이 입력합니다.

   

- log4jdbc는 기존 DB 커넥터를 감싸 로그 기능을 추가해주는 개념이기때문에.. 베이스로 어떤 DB 커넥터를 사용할지          등을 설정해주는 파일이 필요합니다. 저는 두 개의  DB에 모두 로그를 남겨야 하기때문에, 커넥터를 두개 다 넣었습니        다.  만약 둘중 하나만 설정하고 싶으시다면, 설정하고 싶은 DB의 원래 커넥터를 입력하시면 되겠습니다.


3. Config 클래스 생성 후 설정


  - xml등을 사용하여 설정하는 방법도 있지만 스프링은 어노테이션을 사용하면 쉽게 Bean설정을 할 수 있기 때문에

    Config 클래스를 통해 설정해보겠습니다.

( 사실은 이렇게 해보고도 싶고 xml 가독성이 너무 떨어진다고 개인적으로 생각되어 사용했습니다.)

 

파일은 src/main/java/com.xxx.xx의 MVC구조 파일들이 모여있는 곳에 넣어야합니다. 기본적으로 Spring이 실행될 때 src/main/java안에 있는 파일들을 컴파일해 스프링 컨테이너에 Bean을 등록하기때문에, 별도 설정없이 해당 디렉터리를 벗어나면 Bean 자체를 인식하지 못하는 경우가 생깁니다.

 

우선 아까 정한 메인 DB 부터 보겠습니다

package com.example.samyuk.config;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

@Configuration
public class TiberoDataSourceConfig {
    @Bean
    @Primary // setting Multyple DataBase Connection by sangU
    public DataSource tiberoDataSource() {

        return DataSourceBuilder.create()
                .url("jdbc:log4jdbc:tibero:thin:@IP:PORT:DBNAME") // URL을 명시적으로 지정
                .driverClassName("net.sf.log4jdbc.sql.jdbcapi.DriverSpy") // 드라이버 클래스명을 명시적으로 지정
                .username("DB유저 id")
                .password("DB비밀번호")
                .build();
    }

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

        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(firstDataSource);
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath:mappers/*.xml"));//xml파일의 위치, src/main/resources아래에 위치
        return sqlSessionFactoryBean.getObject();
    }

    /**
     * DAO 클래스에서 사용한다.
     **/
    @Bean
    @Primary
    public SqlSessionTemplate firstSqlSessionTemplate(SqlSessionFactory firstSqlSessionFactory) throws Exception {
        final SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(firstSqlSessionFactory);
        return sqlSessionTemplate;
    }
}

코드 자체는 DB 1개를 연동 할때와 비슷하지만 자세히 보면 @Primary라는 어노테이션이 있습니다. 저도 이번 일을 겪으면서  처음 마주한 어노테이션인데요. 스프링 컨테이너에 Bean들을 등록할 때, 동일한 Bean이 두 개 등록 되있으면 어떤 Bean을 가져야할지 모릅니다. 때문에 에러가 발생되는데요. Primary 어노테이션을 붙여놓으면 다른곳에서 Bean 선언시 아무설정이 없다면 기본적으로 Primary어노테이션이 붙은 Bean으로 자동 주입됩니다.

 

때문에 메인 DB와 서브 DB를 정해 놓으라고 저는 말씀드리고 싶었는데요, 만약 자주 사용하는 DB를 Primary로 놓지 않으면 선언시마다 설정을 해주어야합니다. 이러면 효율 자체도 좋지 않고 한 두줄 추가되는 것도 좋지 않을 수 있다고 생각합니다.

 

이제 서브 DB의 config클래스를 살펴보겠습니다.

package com.example.samyuk.config;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

@Configuration
public class MariaDataSourceConfig {
    @Bean(name = "mariaDataSource")
    public DataSource mariaDataSource(){
        return DataSourceBuilder.create()
                .url("jdbc:log4jdbc:mysql://127.0.0.1:3306/test_db")
                .driverClassName("net.sf.log4jdbc.sql.jdbcapi.DriverSpy")
                .username("root")
                .password("12345")
                .build();
    }

    @Bean(name = "mariaSqlSessionFactory")
    public SqlSessionFactory mariaSqlSessionFactory(@Qualifier("mariaDataSource") DataSource dataSource) throws Exception {

        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:mappers/*.xml"));//xml파일의 위치, src/main/resources아래에 위치
        return sqlSessionFactoryBean.getObject();
    }



    /**
     * DAO 클래스에서 사용한다.
     **/
    @Bean(name = "mariaSqlSessionTemplate")
    public SqlSessionTemplate mariaSqlSessionTemplate(@Qualifier("mariaSqlSessionFactory") SqlSessionFactory mariaSqlSessionFactory) throws Exception {
        final SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(mariaSqlSessionFactory);
        return sqlSessionTemplate;
    }
}

크게 다를건 없지만  SessionFactory와 SqlSessionTemplate객체의 인자에 @Qualifier 어노테이션이 존재하는데요, @Qualifier("Bean명")을 하면 괄호 안의 문자에 맞는 이름을 가진 Bean을 가져옵니다. 해당 밑 두개도 서브 DB config클래스에 들어가기때문에, 어떤 DB의 커넥션 DataSource를 넣을지 정해주어야 SqlSessionTemplate선언시 원하는 DB에 붙습니다.

(저는 여기서 한참 해멨습니다...)

 

* 또한 Bean 어노테이션에 name 속성을 추가하면 후에 선언시 해당 속성명으로 Bean을 부를 수 있습니다! 지정하지 않으면 메소드명으로 세팅됩니다.( 따라서 본문의 코드에는 넣으나 안넣으나 마찬가지입니다.)


4. Mapper에 각 DB SQL문 작성


우선 Mapper까지 구분할 필요는 없으나, SQL문법이 조금 다른 언어의 경우 헷갈릴 수 있으니 실제로 사용하실땐 따로 사용하시는 것도 괜찮은거 같습니다. 하지만 해당 글에서는 테스트전용으로 만들기 때문에 같은 mapper에 작성하겠습니다!

 

파일 구조

<?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="mappers.ExampleMapper">
    
    <select id="try36" resultType="com.example.samyuk.vo.CategorySetListVO">
        select 
            *
        from
            CONSENT_CATEGORY_SET_LIST
    </select> <-- 이거 tibero꺼
   
   <select id="testMultiple" resultType="com.example.samyuk.vo.MultipleConnTestVO">
        select
            *
        from
            test_db.user u
    </select> <-- 이거 MySQL

</mapper>

이건 Mybatis를 사용하시는 분들에게 매우 익숙한 부분이라 추가 설명은 하지 않겠습니다.


5. DAO호출 후 정상 작동 확인


DAO에 호출 후 정상적으로 import되는지 확인할 단계입니다. 우선 선언부 코드입니다.

 

package com.example.samyuk.DAO;

import com.example.samyuk.vo.*;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public class TestDAO {

    @Autowired
    private SqlSession sqlSession; // mainDB @Primary로 인해 직접 지정해줄 필요없음

    @Autowired
    @Qualifier(value = "mariaSqlSessionTemplate") // 같은 bean이 2개 이상 존재할 시 어떤 bean을 사용할 건지 지정해야함.
    private SqlSession sqlSession2;         
    // subDB
    public List<MultipleConnTestVO> getTestConn(){
        return sqlSession2.selectList("mappers.ExampleMapper.testMultiple");
    }
    
    public List<CategorySetListVO> get36Test(){
        return sqlSession.selectList("mappers.ExampleMapper.try36");
    }
    
    }

 

보시면 메인DB(Tibero)를 바라보는 SqlSession객체는 자동주입 어노테이션만 존재합니다. 이럴경우 @Primary 어노테이션이 붙은 Bean이 주입됩니다.

 

서브DB(MySQL)을 바라보는 sqlSession2 객체는 @Qualifier 어노테이션을 통해 mariaSqlSessionTemplate이라는 이름의 Bean을 주입합니다. 그렇게 두 개를 작성하고 각 DB에 맞는 SQL Mapper문을 통해 실행하면 됩니다.

 

다들 아시겠지만 IntelliJ나 Eclipse같은 경우 @Qualifier의 value값을 ctrl + 좌클릭시 제대로 연동되면 저희가 설정한 Config클래스로 이동됩니다.

 

그리고 나머지 구조를 통해 DB의 데이터가 잘 출력되는지 확인해보면 됩니다. view를 따로 만들기는 좀 번거로워서 ResponseBody로 전송값만 확인했습니다.

 

다 정상 출력됬네여

 

이상 다중 DB (Multiple DataBase) 연동방법에 대해 알아봤습니다. 혹시 자세한 개념에서 틀린부분이나 지적사항 있으시면 댓글로 말씀해주시면 감사하겠습니다.

 

 

 

 

SOLID, 좋은 코드를 위한 5가지 원칙

글쓰기에 앞서, 해당 글은 김영한님의 스프링 핵심 원리 - 기본편을 참고하여 적었습니다. 수업에서 제가 이해한 대로 적었기 때문에 좋은 마음으로 봐주시고 지적점이 있으면 소중한 댓글 한번 부탁드립니다.

 

SOLID란?

SOLID란 5가지 원칙 각각의 맨 첫 알파벳을 따온 것으로

S - SRP: Single Responsibility Principle (단일 책임의 원칙)

O - OCP : Open Close Principle(개방-폐쇄의 원칙)

L - LSP : Liskov Substitution Principle(리스코프 치환의 원칙)

I - ISP : Interface Sergregation Principle(인터페이스 분리의 원칙)

D - DIP : Dependency Inversion Principle(의존 관계 역전의 원칙)

 

총 5가지로 구성되어있습니다.

 

1. SRP: Single Responsibility Principle (단일 책임의 원칙)

단일 책임의 원칙이란, 하나의 클래스는 하나의 책임만을 가져야한다는 원칙입니다. 예를 들자면 제 자신을 My라 칭하는 클래라 하면 저는 개발자 일때는 개발하기와 디버깅하기 등의 역할,즉 책임이 있고, 가족에서는 막내로서 분리수거하기, 집안일 돕기등의 역할을 가진 구현체, 친구들에게는 같이 놀기, 밥먹기라는 역할을 하는 구현체 총 세개를 가져야한다고 했을때,

My라는 클래스는 3개의 책임을 가지게 됩니다. 이것은 SRP에 위반되기 때문에, 인터페이스를 My개발자, My막내,My친구처럼 3개로 나누어서 하나의 클래스가 하나의 책임을 가지게 해야하는 것이 맞다고 생각합니다.

또한 강의에서는 무언가(Ex:UI)를 변경할 때, 타격이 적을 수록 SRP를 잘 지켰다고 생각하신다 들었습니다. 저도 동의하는 것이 당연히 단일 책임의 원칙을 잘 지켰다면, 기능변경에 따른 변경점은 SRP를 안지켰을 때보다 훨씬 적어진다고 생각합니다. 안 지켜졌다면 나머지 3개의 책임에 대한 구현체의 코드를 모두 변경해야하니까요.

 

2. OCP : Open Close Principle(개방-폐쇄의 원칙)

OCP는 개방-폐쇄의 원칙을 나타냅니다. 무슨 뜻이냐고 깊게 들어가면 기능의 확장에는 열려있으나(구현체) 기능의 변경에는 닫혀있어야(인터페이스)한다고 합니다.

인터페이스에는 구현해야할 기능의 핵심이 담겨있습니다. 예를 들어 볼펜이라는 인터페이스에 색을 구현하는 메서드가 정의되어있으면, A구현체는 색깔을 빨간색으로 구현하고, B구현체는 색깔을 파란색으로 구현하면 이것이 기능의 확장입니다. 또한 기존 Controller에서는 인터페이스에 대한 코드를 전혀 변경할 필요가없습니다. Spring에서는 Component지정을 바꾸거나 @Service 어노테이션만 바꿔달면 됩니다. 하지만 색을 정하는 메서드만 가진 볼펜 인터페이스에 on/off기능을 추가하는 것은 ocp를 위반한다는 것이죠. 이것이 기능을 변경하는 일이 되는 겁니다. 이렇듯 개방 폐쇄의 원칙역시도 신경써야한다는 겁니다.

 

3. LSP : Liskov Substitution Principle(리스코프 치환의 원칙)

LSP는 리스코프 치환의 원칙으로, 상위 객체와 하위 객체를 바꿔도 정상적으로 작동해야하는 원칙을 의미합니다.

자동차에 빗대어 만약 엑셀을 밟을 때 속도가 증가해야하는 목적이지만, 하위객체에서 속도를 감소하게 한다면 정상 작동은 되겠지만 의도를 벗어나는 객체가 되어버립니다. 이런 경우는 리스코프 치환의 원칙을 위반했다 할 수 있습니다.

 

4. ISP : Interface Sergregation Principle(인터페이스 분리의 원칙)

ISP는 인터페이스 분리의 원칙으로, 위의 SRP(단일 책임의 원칙)과 내용이 비슷합니다. 범용 인터페이스로 모든 것을 구현하는 것보다, 특정 기능들에 맞춰 여러개의 인터페이스로 구성해야한다는 것이죠, 만약 하나의 인터페이스에 키보드의 기능, 마우스의 기능을 모두 넣어놓으면 구현체에서 전부 다 구현을 하긴 해야하기때문에, 각각의 인터페이스로 분리하는 것이 ISP를 잘 지키고 있다고 할 수 있습니다.

 

5. DIP : Dependency Inversion Principle(의존 관계 역전의 원칙)

의존 관계는 사실 인터페이스를 부르면 구현체를 불러와 해당 구현체에 대한 코드가 실행되지만, 구현 클래스에 의존하지 말고 인터페이스(역할)에 의존하라는 원칙입니다. 만약 구현체에 의존하게 되면 이후 유지보수나 업데이트시 기능변경이 매우 어려워집니다.(개발의 중점이 구현체에 있으면 인터페이스의 변경이 어려워 질 수 있기 때문인것 같습니다.)

 

이상으로 SOLID에 대한 설명이 모두 끝났는데요, 글을 작성하면서 나는 여태까지 어떻게 설계했었는지를 되돌아보게 되었음과 동시에 적었는데도 개념이 잘 안잡히는 부분들이 있는것 같습니다. 앞으로도 여러번 상기해봐야할 SOLID원칙입니다. 끝까지 봐주셔서 감사합니다!

IntelliJ 외장 톰캣에 배포하고 실행하기 

 

오늘은 IntelliJ라는 JAVA를 다루는 분들이라면 많이 사용하시는 IDE를 이용하여 프로젝트를 war파일로 배포하고, 외장 톰캣에 배포하려고합니다. Legacy프로젝트는 war파일로 배포하는것이 굉장히 쉬웠는데, spring boot 프로젝트는 몇가지 변경점이 있고 내장, 외장 톰캣 구동에 차이가 있지만 외장 톰캣 배포방법에 대해서만 다루겠습니다.

 

이 글을 보시는 분들은 대부분 프로젝트의 테스트를 마무리하고 배포하시려는 분들이 많다고 생각됩니다. 따라서 기존 프로젝트가 있으시면 2번부터 보시면 되겠습니다.

 

1. 프로젝트 생성

우선 배포할 프로젝트가 있어야합니다. 스프링 부트 프로젝트 생성법은 하단 링크를 참고해주세요.

2023.07.14 - [JAVA] - [Spring] Spring boot 프로젝트 생성 방법

 

[Spring] Spring boot 프로젝트 생성 방법

Spring boot란? - Spring Boot란 Spring Framework 프로젝트 생성시 처음하면 매우매우 복잡하고 절대로 쉽게 할 수 없는 초기 설정 및 라이브러리 세팅을 간단하게 만들어주는 프레임워크(FrameWork)입니다. -

lucky-web.tistory.com

 

2. build.gradle 수정

우선 빌드하실때 build에 들어가면 해당 디렉토리들이 보이실텐데요. bootWar이나 bootJar은 내장 톰캣에서 프로젝트를 구동할 때 사용됩니다. 때문에 밑에 있는 war이나 jar로 배포해야한다는 점 미리 말씀드리겠습니다.

 

해당 과정을 실행하기전, build.gradle을 변경해주어야합니다.우선

plugins {
id 'java'
id 'war' //추가
id 'org.springframework.boot' version '2.7.14'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

이부분은 첫 사진의 war라는 이름의 메뉴를 추가해주는 부분입니다.

apply plugin : 'war'

또한 이 부분을 build.gradle에 추가합니다. 맨 윗사진의 war라는 이름의 플러그인을 실행할 수 있게해주는 구문입니다.

 

// 내장톰캣 전용 war파일과 외장전용의 충돌 방지
bootWar.enabled = false
war.enabled = true

위 부분은 위에서 설명드린 것과 같이 내장톰캣전용인 bootWar의 빌드를 제한해주고 war파일을 만들게 해주는데, 두가지가 충돌하지 않게 직접 사용도를 지정한겁니다. 내장용으로 받으실거면 bool값을 반대로 주시면되겠죠?

 

war{
archivesBaseName = 'test2'
archiveFileName = 'test2.war'
archiveVersion = '0.0.0'
}

해당 부분은 배포될 war파일의 파일명, 버젼등을 지정하는 부분입니다.

 

이렇게 되면 build.gradle파일의 수정은 완료됩니다. 전체 코드 사진을 보여드리고 다음 단계로 넘어가겠습니다.

plugins {
id 'java'
id 'war'
id 'org.springframework.boot' version '2.7.14'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

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

// 내장톰캣 전용 war파일과 외장전용의 충돌 방지
bootWar.enabled = false
war.enabled = true

java {
sourceCompatibility = '1.8'
}

war{
archivesBaseName = 'test2'
archiveFileName = 'test2.war'
archiveVersion = '0.0.0'
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

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

 

 

3. SpringBootApplication.java 수정

외장 톰캣에 배포하여 사용하려면 위 클래스 파일을 아래와 같이 수정해야합니다. 

package com.example.test2;

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 Test2Application extends SpringBootServletInitializer { // 추가

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

@Override // 추가
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Test2Application.class);
}

}

해당 부분은 application.sources로 받은 클래스를 외부 servlet에 배포할 수 있게 변환하여 주는 역할을 수행합니다. 따라서 내장 톰캣용 배포를 하시는 분들은 필요가없지만. 외장 서버에는 서블릿을 초기화하고 설정을 등록하는 것이 필요하기때문에 해당 메소드를 추가해야합니다.

 

 

4. war파일 빌드

해당 설정까지 완료하셨으면 아래 사진과같이 gradle 메뉴를 열고 war메뉴를 실행하시면됩니다.

war 실행

그리고 프로젝트 경로/build/libs 파일에 보시면 제가 설정한대로 .war파일이 생성되었습니다.

프로젝트/build/libs

war{
archivesBaseName = 'test2'
archiveFileName = 'test2.war' // 설정한 파일명
archiveVersion = '0.0.0'
}

 

4. 외장 톰캣에 파일 옮기기

이후 설치한 외장 톰캣 파일의 webapps 옮기시면됩니다. 아래 사진같은 경우는 제가 따로 실행시킨 test.war파일이 있습니다.

또한 5번에서 세팅을 할 것인데. 이런 방법 없이 바로 실행되었으면 좋겠다 하시는 분들은 war파일을 root.war로 바꿔서 넣으시면됩니다. 기본적으로 톰캣은 root.war을 바라보고  있기 때문입니다.

톰캣 파일의 webapps에 war파일 옮기기

이후 톰캣 경로/conf파일의 server.xml에 들어가 설정을 해주어야합니다.

제일 밑에 있는 <Host></Host>부분에

<Context path="/test2" docBase="test2" reloadable="true" />

라는 부분을 추가할 것인데. path는 localhost:포트번호 다음에 입력할 프로젝트의 경로를 지정하는 것입니다.

docBase는 어떤 파일을 베이스로 가지고갈지 결정하는 것입니다.(war파일명 적으면됩니다.)

reloadable은 war파일을 업데이트해서 바꿔치기를 하면 적용이 바로 되도록하는 것입니다. 개발시에는 true가 편하고, 운영시에는 당연히 false로 놓아야합니다.

 

아래는 server.xml의 전체 코드입니다.

<?xml version="1.0" encoding="UTF-8"?>
<!--
  Licensed to the Apache Software Foundation (ASF) under one or more
  contributor license agreements.  See the NOTICE file distributed with
  this work for additional information regarding copyright ownership.
  The ASF licenses this file to You under the Apache License, Version 2.0
  (the "License"); you may not use this file except in compliance with
  the License.  You may obtain a copy of the License at


  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
-->
<!-- Note:  A "Server" is not itself a "Container", so you may not
     define subcomponents such as "Valves" at this level.
     Documentation at /docs/config/server.html
 -->
<Server port="8005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <!-- Security listener. Documentation at /docs/config/listeners.html
  <Listener className="org.apache.catalina.security.SecurityListener" />
  -->
  <!-- APR library loader. Documentation at /docs/apr.html -->
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <!-- Prevent memory leaks due to use of particular java/javax APIs-->
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <!-- Global JNDI resources
       Documentation at /docs/jndi-resources-howto.html
  -->
  <GlobalNamingResources>
    <!-- Editable user database that can also be used by
         UserDatabaseRealm to authenticate users
    -->
    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
  </GlobalNamingResources>

  <!-- A "Service" is a collection of one or more "Connectors" that share
       a single "Container" Note:  A "Service" is not itself a "Container",
       so you may not define subcomponents such as "Valves" at this level.
       Documentation at /docs/config/service.html
   -->
  <Service name="Catalina">

    <!--The connectors can use a shared executor, you can define one or more named thread pools-->
    <!--
    <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
        maxThreads="150" minSpareThreads="4"/>
    -->


    <!-- A "Connector" represents an endpoint by which requests are received
         and responses are returned. Documentation at :
         Java HTTP Connector: /docs/config/http.html
         Java AJP  Connector: /docs/config/ajp.html
         APR (HTTP/AJP) Connector: /docs/apr.html
         Define a non-SSL/TLS HTTP/1.1 Connector on port 8080
    -->
    <Connector port="8082" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443"
               maxParameterCount="1000"
               />
    <!-- A "Connector" using the shared thread pool-->
    <!--
    <Connector executor="tomcatThreadPool"
               port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443"
               maxParameterCount="1000"
               />
    -->
    <!-- Define an SSL/TLS HTTP/1.1 Connector on port 8443
         This connector uses the NIO implementation. The default
         SSLImplementation will depend on the presence of the APR/native
         library and the useOpenSSL attribute of the AprLifecycleListener.
         Either JSSE or OpenSSL style configuration may be used regardless of
         the SSLImplementation selected. JSSE style configuration is used below.
    -->
    <!--
    <Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
               maxThreads="150" SSLEnabled="true"
               maxParameterCount="1000"
               >
        <SSLHostConfig>
            <Certificate certificateKeystoreFile="conf/localhost-rsa.jks"
                         type="RSA" />
        </SSLHostConfig>
    </Connector>
    -->
    <!-- Define an SSL/TLS HTTP/1.1 Connector on port 8443 with HTTP/2
         This connector uses the APR/native implementation which always uses
         OpenSSL for TLS.
         Either JSSE or OpenSSL style configuration may be used. OpenSSL style
         configuration is used below.
    -->
    <!--
    <Connector port="8443" protocol="org.apache.coyote.http11.Http11AprProtocol"
               maxThreads="150" SSLEnabled="true"
               maxParameterCount="1000"
               >
        <UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" />
        <SSLHostConfig>
            <Certificate certificateKeyFile="conf/localhost-rsa-key.pem"
                         certificateFile="conf/localhost-rsa-cert.pem"
                         certificateChainFile="conf/localhost-rsa-chain.pem"
                         type="RSA" />
        </SSLHostConfig>
    </Connector>
    -->

    <!-- Define an AJP 1.3 Connector on port 8009 -->
    <!--
    <Connector protocol="AJP/1.3"
               address="::1"
               port="8009"
               redirectPort="8443"
               maxParameterCount="1000"
               />
    -->

    <!-- An Engine represents the entry point (within Catalina) that processes
         every request.  The Engine implementation for Tomcat stand alone
         analyzes the HTTP headers included with the request, and passes them
         on to the appropriate Host (virtual host).
         Documentation at /docs/config/engine.html -->

    <!-- You should set jvmRoute to support load-balancing via AJP ie :
    <Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">
    -->
    <Engine name="Catalina" defaultHost="localhost">

      <!--For clustering, please take a look at documentation at:
          /docs/cluster-howto.html  (simple how to)
          /docs/config/cluster.html (reference documentation) -->
      <!--
      <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
      -->

      <!-- Use the LockOutRealm to prevent attempts to guess user passwords
           via a brute-force attack -->
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <!-- This Realm uses the UserDatabase configured in the global JNDI
             resources under the key "UserDatabase".  Any edits
             that are performed against this UserDatabase are immediately
             available for use by the Realm.  -->
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>

      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
           <Context path="" docBase="test" reloadable="true" />
           <Context path="/test2" docBase="test2" reloadable="true" /> //추가
           <!-- 새로운 프로젝트 추가 -->

        <!-- SingleSignOn valve, share authentication between web applications
             Documentation at: /docs/config/valve.html -->
        <!--
        <Valve className="org.apache.catalina.authenticator.SingleSignOn" />
        -->

        <!-- Access log processes all example.
             Documentation at: /docs/config/valve.html
             Note: The pattern used is equivalent to using pattern="common" -->
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />

      </Host>
    </Engine>
  </Service>
</Server>

 

 

6. Tomcat 실행 및 디렉터리 생성 확인

이제 톰캣경로/bin에 들어가 Window는 startup.bat, MacOs는 startup.sh를 눌러 실행시킨 후 webapps에 들어가 추가한 war파일과 이름이 같은 디렉터리가 생기는지 확인하고, startup에서 로그를 확인하며 에러가 발생하지 않는지 확인합니다.

우선 디렉터리가 생겼으면 반쯤 성공했다고 보시면됩니다.

톰캣 실행 후
war파일과 이름이 같은 디렉터리 생성 확인

이제 웹에서 잘 나오는지 확인해보겠습니다. 저는 server.xml에서 localhost:포트번호/프로젝트명/test2를 입력하면 index파일이 나오게 설정해놓았습니다. 

잘나온다!

또한 미리 등록되있는 프로젝트는 포트명까지만 입력하면 작동되는 기본 프로젝트이므로, 그역시 작동되는지 확인하겠습니다.

모두 잘 작동되는 것을 확인했습니다. 이것으로 Spring Boot 프로젝트 외장 톰캣 배포에 대한 포스팅을 마치겠습니다.

실무에서 내부 서버의 파일을 가져와 회사 프로그램을 통해 JPG를 오픈해야하는 일이 있었다. 이미지 파일이 없을때는 해당 유저의 DB에 있는 이미지의 byte값을 가져와 로컬주소(EX : C://test/file)로 남기게 url을 세팅하고, 클라이언트 측에서는 IP주소(Ex:127.0.0.1:8080/file)에서 가져오게 세팅하니 URL에 대한 오류는 없었다.

하지만 문제는 파일명을 DB에서 유저의 ID를 가져와 세팅했을때, 공백이 존재했다. 하지만 회사프로그램 뷰어에서 작동했을때 가져오는 URL도 공백을 포함한 파일명을 지정하면 잘 가져와졌지만, 클라이언트내부에서 프로그램 뷰어를 통해 가져올땐 가져와지지 않았다. 한참을 찾아도 로직의 잘못된 것을 느끼지못했다. 하지만 파일명을 지정하는 부분부터 모든곳에 .TRIM()메서드를 통해 공백을 제거한 후 테스트하니 정상적으로 출력되었다. 파일명을 세팅하거나 가져올때는 항상 공백을 조심하자.

 

개발을 하면서 무언가 틀린점이 없다고 생각이 들때, 내가 틀리게 생각했다고 믿지말고 기본적인 부분을 꼼꼼하게 다시본다는 생각을 해야할거같다.

JPA 연동하기

JDK : 1.8
Spring Boot : 2.3.1

JPA : Hibernate

DBMS : MySQL

 

오늘은 JPA연동하는 방법에 대해서 포스팅을 하려고합니다.

단순 연동 방법이 필요하신 분들은 서론을 건너뛰고 보시면 될 것같습니다.

JPA란?

JPA란 MyBatis랑 비슷하게 DB와 서버의 연결을 간편하게 해주는 인터페이스입니다. 하지만 Mybatis와 다르게 엔티티에 직접 테이블과 컬럼을 지정하고 쿼리문을 객체지향적으로 코딩할 수 있게 도와줍니다.

 

JPA의 구성

JPA의 역할을 이해하기 쉽도록 그림을 한장 퍼왔습니다. 위 설명과 조합하시면 이해가 빠르실 것 같습니다.

출처 : https://velog.io/@tmdgh0221/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-%EC%A0%95%EB%A6%AC

그림을 보시면 JPA라는 인터페이스로 JDBC를 감싸고 있습니다. 저희가 JPA의 함수와 문법을 이용해서 쿼리문을 날려달라고 요청하면, JPA는 JDBC에 맞게 해당 요청을 쿼리문으로 변환하여 DB와 연동합니다. 

 

왜 MyBatis에서 JPA로 넘어가나요?

개인적인 생각으로는 MyBatis는 Mapping파일을 경로로 잡는 config파일을 xml로 만들고 또한 Mapper파일을 새로 생성하여 쿼리문을 작성하기때문에 객체지향적인 설계와는 거리가 멀고, 엔티티를 쉽게 사용하려면 별칭으로 등록하고 또한 그마저도 사소한 오타가 존재할 시 오류가 발생합니다. JDBC를 직접 사용하는 것보다는 낫겠지만 그래도 개발시에 상당히 힘들고, 유지보수 역시 하나하나 뒤져보면서 Entity의 값과 name을 비교하는것이 많이 힘듭니다.

 

하지만 JPA는 엔티티에서 그대로 컬럼, 테이블을 지정하고, MVC의 Model부분에서 객체지향적으로 쿼리문을 날리면 작성자의 목적에 맞게 변환시켜 데이터를 CRUD할 수 있게 되어있기 때문에 간편하고, 세팅이나 오류나 발생할 부분이 적어서 확실히 개발 효율을 증가시킬 수 있을 것같습니다.

 

Hibernate(JPA 구현체)

오늘 포스팅에서는 Hibernate라는 JPA 인터페이스의 구현체를 사용할 것입니다. 위 설명대로 JPA는 인터페이스 이기때문에, 작성되어있는 메서드를 구현해야합니다. 이런 것들을 이미 구현해놓은 구현체를 사용함으로서 쉽게 사용할 수 있습니다. 물론 Hibernate말고도 EclipseLink, DataNucleus, OpenJPA, TopLink Essentials등 여러가지가 존재하니, 상황에 맞게 사용하시면 되겠습니다.

 

JPA 연동방법

우선 스프링 부트 프로젝트를 생성해야합니다. 모르시는 분들은 밑의 글을 참고해주세요, 여러 설정중에 JPA 연동과 관련된 라이브러리는 Spring-data-JPA와 사용할 DB의 connector입니다. 참고 Spring-data-JPA만 넣으시면 자동으로 hibernate에 대한 라이브러리도 추가됩니다.

2023.07.14 - [JAVA] - [Spring] Spring boot 프로젝트 생성 방법

 

[Spring] Spring boot 프로젝트 생성 방법

Spring boot란? - Spring Boot란 Spring Framework 프로젝트 생성시 처음하면 매우매우 복잡하고 절대로 쉽게 할 수 없는 초기 설정 및 라이브러리 세팅을 간단하게 만들어주는 프레임워크(FrameWork)입니다. -

lucky-web.tistory.com

Spring Data JPA와 Hibernate에 대한 라이브러리 추가
DB 커넥터 라이브러

 

그 이후 생성된 프로젝트의 외부 라이브러리를 확인하면, hibernate와 JPA에 대한 라이브러리를 확인할 수 있습니다.

hibernate관련 라이브러리
Spring Data JPA 관련 라이브러리

 

외부 라이브러리를 확인하셨다면 src/resources 디렉터리 밑에 application.properties파일을 생성하고 해당 내용을 작성합니다.

application.properties추가

 

application.properties 내용

하나씩 보시면 server.port는 연결할 서버의 포트, url은 jdbc와 커넥션할 DB 스키마의 주소(JDBC에 사용하는 url이나 Mybatis에서 설정해놓은 url과 같습니다.) 유저 아이디와 비밀번호, 그리고 연결할 때 사용하는 드라이버 주소까지 입력합니다.

 

그리고 그 밑은 어떤 DB를 사용하는지 지정하는 부분이고, Database-platform역시 같습니다.

 

3번째는 어떤 단계의 로그부터 남길지를 정하는 단계로, log4j의 여러단계에서 용도에 맞게 지정하시면됩니다.

 

이런 설정들은 더 많이 있지만 필요할 때 마다 알아보시면서 추가하시면 좋을 것같습니다.

 

 

 

 

이제는 엔티티(Entity)와 DB 테이블을 연동하겠습니다. 우선 가상의 테이블을 만들겠습니다.

가상의 테이블

member테이블은 pid,username,name 총 3가지의 컬럼을 가지고 있는 테이블입니다. 해당 테이블을 연결하는 엔티티는 당연히 컬럼의 갯수에 맞게 만들어야겠죠?

@Getter // lombok
@Builder // lombok
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity(name="member")// 테이블 명 작성
public class MemberEntity {

    @Id // pk
    @GeneratedValue(strategy = GenerationType.IDENTITY) // == 시퀀스
    private long pid;

    @Column(nullable = false,unique = true,length = 30) // 속성값 정하기
    private String username;

    @Column(nullable = false, length = 100)
    private String name;

    public MemberEntity(String username, String name) {
        this.username = username;
        this.name = name;
    }
}

테이블 member를 바라보는 MemberEntity클래스를 생성했습니다. @Entity(name = "member")어노테이션은 연결된 DB의 member라는 이름을 가진 테이블과 연동한다는 뜻입니다.

- @Id는 해당 컬럼이 pk라는 것을 의미합니다.

- @GeneratedValue(strategy = GenerationType.IDENTITY)는 MySQL의 auto_increment나 Oracle의 sequence와 같이 자동 숫자 증가를 나타내게 하는 어노테이션인데, GenerationType을 바꿈에 따라 여러가지로 조정할 수 있습니다.

- @Column은 테이블의 컬럼이라는 뜻을 나타내고, 컬럼의 여러가지 속성을 제어할 수 있습니다.

 

이제 JPA의 기능을 사용하기 위한 준비를 하겠습니다.

 

JPARepository생성

 

 

Interface생성

JPA를 끌어올 인터페이스를 생성하고, JPARepository를 상속받습니다.

package com.example.demo.repo;

import com.example.demo.entity.MemberEntity;
import org.springframework.data.jpa.repository.JpaRepository;


public interface MemberRepository extends JpaRepository<MemberEntity, Long> {

}

따로 입력하시지 않아도 됩니다. 그저 JPA의 기능을 끌어쓰기 위한 인터페이스이기때문입니다.

 

 

JPA 작동

 

이제 JPA를 사용하기만 하면됩니다. 보통은 Service에 구현하여 데이터를 가져오고 원하는 방식으로 수정하기도 하지만, 지금은 작동을 보여드리기위하여 Controller에 바로 JPARepository의 의존성을 주입하고 사용해보겠습니다.

 

Controller

package com.example.demo.controller;

import com.example.demo.entity.MemberEntity;
import com.example.demo.repo.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController// @ResponseBody + @Controller
@RequiredArgsConstructor // final 객체를 Constructor Injection해줌.( like @Autowired)
@RequestMapping("/v1")
public class MemberController {
    private final MemberRepository repository;
    /*
    * 멤버조회
    * @return
    * */

    @GetMapping("member")
    public List<MemberEntity> findAllMember(){
        return repository.findAll();
    }

    /**
     * 회원가입
     *
     * @return
     */
    @PostMapping("member")
    public MemberEntity signUp() {
        final MemberEntity member = MemberEntity.builder().username("test_user@gmail.com")
                                                          .name("test user")
                                                          .build();
        return repository.save(member);
    }
}

코드를 보시면 방금 생성한 MemberRepository의 의존성을 주입하고, JPA에서 findAll이라는 함수를 사용하여 해당 값을 responseBody에 return하도록 되어있습니다.

findAll함수를 사용하면 어떤 쿼리문이 작동될까요?

우선 인터페이스 코드를 다시보면...

package com.example.demo.repo;

import com.example.demo.entity.MemberEntity;
import org.springframework.data.jpa.repository.JpaRepository;


public interface MemberRepository extends JpaRepository<MemberEntity, Long> {

}

MemberEntity를 JpaRepository에 담음으로서 해당 인터페이스는 member테이블을 바라보게 되어있습니다.

findAll은 해당 테이블의 모든 데이터를 조회하기때문에

select

          *

from

          member;

쿼리문이 실행되는 것입니다.

그럼 현재 DB에있는 데이터와 웹에서 리턴받는 데이터가 같은지 확인해보겠습니다.

5개의 데이터가 존재한다.

총 5개의 데이터가 존재하는것을 볼 수 있습니다. 그럼 웹에서 findAll()함수를 사용하는 페이지로 가서 5개가 나오는지 확인해보겠습니다.

 

총 5개가 나오는 것을 볼 수 있습니다. 이렇게 JPA의 세팅과 사용방법에 대해 다뤄보았습니다.

Spring boot란?

- Spring Boot란 Spring Framework 프로젝트 생성시 처음하면 매우매우 복잡하고 절대로 쉽게 할 수 없는 초기 설정 및 라이브러리 세팅을 간단하게 만들어주는 프레임워크(FrameWork)입니다.

 

- 학원에서 STS를 통해 스프링 레거시 프로젝트(Spring Legacy Project)를 할 때 매우 오래걸린 초기세팅이 여기선 단 몇번의 클릭으로 끝나는 것을 보고 많이 놀랐던 것 같습니다. 이제 레거시 프로젝트와 스프링 부트 프로젝트의 세팅을 비교해보겠습니다.

 

Spring Boot 프로젝트 생성

우선 IDE는 스프링 부트(Spring boot)를 다루실 때 많이들 사용하시는 IntelliJ를 기준으로 말씀드리겠습니다.

제가 아는 스프링 부트 생성 방법은 두가지로

- intelliJ에서 직접 스프링 부트 프로젝트 생성하기

- Spring.start.io에서 만들어 IntelliJ에서 임포트하기

입니다. IntelliJ에서 생성하는 방법 먼저 보여드리겠습니다.

우선 프로젝트 새로 만들기에서 프로젝트를 클릭합니다.

프로젝트.. 클릭

그럼 다음 화면이 나타나게 되는데 왼쪽 제너레이터(Generator)에서 Spring Initializr를 누르면 됩니다.

스프링 이니셜라이즈 선택

그러면 해당 화면에서 프로젝트명, 저장 경로, 사용언어 및 빌드툴,JDK버젼을 선택하고 다음을 눌러줍니다.
빌드 툴은 보통 Gradle을 추천드리는데, Maven보다 훨씬 빌드속도가 빠르고 잠시 후 보여드리겠지만 라이브러리 추가시의 가독성 역시 훨씬 뛰어납니다.

각종 옵션 추가

다음을 누르면 아래의 사진 처럼 화면이 나옵니다. 초기에 추가해놓을 옵션들과 부트의 버젼을 선택합니다.

이 부분에서 기본 Spring과 많은 차이가납니다. MVNRepository에서 하나하나 찾고 맞는 버젼에 따라 가져와 입력하는 것이 아닌, 기능만 선택하면 해당 라이브러리를 자동으로 세팅해주는 것입니다.

부트 버젼과 기능 선택

대표적으로 몇가지 말씀드리면 Developer Tools는 개발의 편의성을 위한 툴을 의미합니다. 대표적으로 Lombok은 엔티티등을 작성할 때 어노테이션 하나만으로 수많은 반복코드를 작성하지 않아도 되도록해줍니다.

Web은 말 그대로 Web에 관한 기능들입니다. SpringWeb부터 Spring session등.... 세부적인 것들은 검색하셔서 필요한 기능을 추가하시면 됩니다.

그 이외에도 view단과 서버단을 연결하는 Template Engines, 보안 기능의 Security, 관계형 DB 관련 기능인 SQL등등 필요한 것들을 이 화면에서 추가한 뒤 생성을 누르면 됩니다. 저는 해당 글을 위해 기본적인 것들만 추가한 사진을 보여드리겠습니다.

추가한 기능은 오른쪽 하단에 나옵니다.

보시면 Spring Web과 Lombok, MySQL과 통신할때 사용하는 MySQL Driver그리고 Spring Data JPA등 6개의 기능을 넣었습니다. 그럼 생성해볼까요?

프로젝트가 벌써 생성됬다고?!

보시면 빠르게 프로젝트가 생성되었습니다. 이제 Maven의 Pom.xml과 같은 기능인 build.gradle에서 정상적으로 기능이 추가가 되었는지 확인하겠습니다.

정말로 추가가되다니...

보시면 JPA와 JDBC, SpringWeb,lombok등 제가 추가한 기능들이 모두 추가되어 빌드까지 되어있는 상태를 확인할 수 있습니다. 또한 Gradle은 Maven과 다르게 하나의 기능당 거의 1줄만 사용하고있는데요. 빌드 속도도 물론 빠르지만 가독성도 우수해 기능이 많아져도 비교적 관리가 편할 것입니다.

 

이번엔 Spring.io에서 생성하는 것을 다루겠습니다. import하는 것만 다르기 때문에 빠르게 짚고 넘어가겠습니다.

우선 해당 링크에 접속합니다.

https://start.spring.io/

그러면 아래 사진과 같은 화면이 나옵니다.

부트 버젼, 언어, 빌드툴과 프로젝트 명을 선택한 후 추가할 라이브러리는 Dependencies옆 버튼을 클릭합니다.

 

Dependencies 추가

여기서 IntelliJ에서 추가했던 것처럼 lombok, Spring DevTools등을 추가하신 후

GENERATE 버튼을 클릭하면 해당 프로젝트가 다운됩니다. 이후 다운 받은 파일을 IDE에서 임포트해서 열면 동일하게 생성된 프로젝트를 확인할 수 있습니다.

 

이렇게 오늘은 Spring Boot 프로젝트를 생성하는 법을 다뤄보았는데요, 많이 편리해진건 사실이지만 최근에 부트 프로젝트를 이리저리 건드려보니 귀찮더라도 결국 부딪히면서 Spring이나 JAVA에 대한 이해도를 높인 후 편의기능을 사용하는 것이 맞다는 생각이 듭니다. 이상으로 포스팅을 마치겠습니다.

이 문제는 입력에 4<n<1000 범위 안에 있는 4의 배수가 입력으로 들어옵니다. 또한 출력은 다음 사진과 같이 입력의 수를 4로 나누었을때 나오는 몫만큼 long을 출력하고 마지막에 int를 붙여야합니다.

출력 예시 -- 백준

그럼 우선 입력을 받기전 어떤 식으로 문제를 풀어야할지 생각해보아야합니다.

첫 번째로 입력된 값을 저장하는 변수가 있어야합니다.

 

두 번째로 for문의 작동 횟수를 조절해주는 값을 구해서 변수로 이용하면 편리할 것입니다.

세 번째로 출력할 값을 저장하는 문자열 변수가 존재해야합니다.

네 번째로 long이라는 값이 반복되기 때문에 for문을 이용하여 변수에 반복적으로 값을 삽입합니다.

 

순서대로 한번 작문해보겠습니다.

 

첫 번째로 입력값을 받고 그 값을 변수에 저장하겠습니다.

public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);// 값을 받는 객체
int n = sc.nextInt();//바이트의 값이 들어온다;
}

스캐너 객체를 통해 몇 바이트를 계산할 것인지 정수형 데이터로 입력을 받고, 그 값을 정수형 변수에 저장해놓습니다.

 

두 번째로 입력한 값을 4로 나누어 long 을 몇번 출력해야 하는지 숫자를 계산하여 변수에 저장합니다. 이 변수는 후에 반복(for)문에서 사용 될 것입니다.

public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);// 값을 받는 객체
int n = sc.nextInt();//바이트의 값이 들어온다;

int num = n / 4;// long을 찍어야하는 횟수를 계산하여 저장한다.
}

int num을 통해 n에 값이 20이 들어온다면 5가 들어올 것입니다. 그럼 이 변수를 for문에 삽입하여 for문이 5번 작동하도록 할 수 있을 것입니다.

 

세 번째로 출력해야하는 문자열 값을 저장하는 변수를 만들어줍니다.

import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);// 값을 받는 객체
int n = sc.nextInt();//바이트의 값이 들어온다;

int num = n / 4;// long을 찍어야하는 횟수를 계산하여 저장한다.

String word = ""; // 문장을 저장하는 변수를 가진다.
}

String word를 선언하고 우선 공백을 값으로 지정해줍니다. 이 변수가 마지막에 정답으로 출력될 것입니다.

 

네 번째로 for문을 통해 계산된 횟수만큼 word변수에 long을 추가합니다.

import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);// 값을 받는 객체
int n = sc.nextInt();//바이트의 값이 들어온다;

int num = n / 4;// long을 찍어야하는 횟수를 계산하여 저장한다.

String word = ""; // 문장을 저장하는 변수를 가진다.

for (int i = 0; i < num; i++) {
word += "long ";// for문을 돌면서 4비트마다 long을 하나씩 저장한다.
}
}

해당 for문을 통해 num에 저장된 수만큼 long이 word변수에 추가되어 저장될 것입니다. 이제 거의 마지막 단계입니다.

하지만 위 출력 예시를 잘 보시면 long과 long, long과 int사이엔 모두 띄어쓰기가 한칸 씩 존재합니다. 따라서 for문에서 값을 삽입할 때 long 뒤에 반드시 띄어쓰기 한칸을 넣어주셔야 요구하는 답이 나오게됩니다.

 

이젠 마지막으로 for문뒤에 int를 삽입하고 그 값을 출력합니다.

import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);// 값을 받는 객체
int n = sc.nextInt();//바이트의 값이 들어온다;

int num = n / 4;// long을 찍어야하는 횟수를 계산하여 저장한다.

String word = ""; // 문장을 저장하는 변수를 가진다.

for (int i = 0; i < num; i++) {
word += "long ";// for문을 돌면서 4비트마다 long을 하나씩 저장한다.
}

word += "int"; //문장의 마지막에 int를 붙인다.
System.out.println(word);//제대로 출력이 되는지 확인한다.
      }
}

한번 콘솔에서 20을 찍고 long이 5번 출력되고 int가 정확하게 출력되는지 테스트해보겠습니다.

테스트 콘솔 창

보시면 값을 20을 입력했을 때 long이 5번 출력되고, 마지막에 int가 정상적으로 출력되는 것을 알 수 있습니다.

 

부연설명

20의 값을 콘솔에 입력하면 변수 n에는 20이 저장되고, 변수 num에는 20을 4로 나눈 몫인 5가 저장됩니다.

그럼 for문에서 i는 0부터 num보다 작을 때까지만 실행하게 되어있기 때문에 0, 1, 2, 3, 4 총 5회가 동작되면서 

문자열 변수 word에는 "long "이 총 5번 더해지고 for문의 동작이 종료됩니다. 이후 변수 word에 int가 마지막에 출력됨으로서 모든 값을 다 구한 이후 System.out.println을 통해 출력하여 답변을 제출합니다.

 

위 코드를 제출하여 정답을 맞춘 사진입니다.

아마 buffer를 이용하면 좀 더 빠르게 방법을 구현할 수 있다고 하는데 다음에 한번 해봐야겠습니다.

모자란 글 봐주셔서 감사합니다.

'백준' 카테고리의 다른 글

백준 2439번 - 별 찍기-2  (0) 2023.02.02
백준 11022번 A+B-8  (0) 2023.01.03

NetworkOnMainThreadException이란?

-NetworkOnMainThreadException은 안드로이드의 기본 activity에서 network 관련 API를 직접 사용했을때 발생합니다. 저같은 경우에는 회사에서 하이브리드앱에 뷰어를 열어 문서를 저장할 때 메인스레드로 JDBC API로 DB에붙으려다가 발생한 경우입니다.

 

해결법

- 매우 간단합니다. 해당 API작동을 메인스레드가 아닌 별도의 스레드를 선언하여 작동시키면 문제없이 작동합니다.

예를 들어 

	@Override
	public void eventReceived(Object sender, ResultEventArgs event) {
			이벤트 로직;
                	JDBC 로직;
                }

해당 코드처럼 실행하면 NetworkOnMainThreadException가 발생하지만, JDBC부분을 아래 코드처럼 새로운 스레드에서 작동하게 선언하면됩니다.

	@Override
	public void eventReceived(Object sender, ResultEventArgs event) {
			이벤트 로직;
      		new Thread(() -> {
                	JDBC 로직;
                    })
                }

간단하지만 앱 자체를 다뤄본적 없는 저에게 상당히 생소한 예외였고, 이 포스팅으로 간단하게 해결할 수 있으시면 좋겠습니다.

+ Recent posts