-
[diary] 부트캠프 시작 후 더욱 열정이 불타오르는 사람
intro : 부트캠프 첫주 후에 한주 쭉 쉬는거 너무 좋다.
부트캠프를 시작한지 한주가 지나가고 난 뒤 첫 주말을 맞이 하였다. 정신없이 시작한 한주였기에 피로감이 굉장히 심했었고, 금요일 저녁에 본가로 내려가는 루틴을 앞으로도 매주 해야하기에 수업이 끝난 금요일 저녁, 집으로 가는길은 쉽지 않았다.
(힘든표정으로 집가는 날 보는 주변 사람들 표정이 딱 이랬겠지?)
어찌저찌 집에와서 짐을 풀고 나서 시간을 보니 9-10시 사이였고, 잠에들어야 하는 시간이었던 것만 기억이 난다. 그렇게 금요일은 제대로 즐기지도 못한채 주말이 되어서 토요일인지 일요일인지 구분조차 못해서 주말에 글을 못쓰고 오늘이 난 일요일인줄 알았던 레전드 사건이 발생해버렸다.
(나 진짜 오늘 일요일인줄 알았다.)
아니 주말에 글써야하는데, Git 커밋도 찍어야하고, 할게 많았는데 주말에 잠자고 쉬느라고 온 정신을 쏟아버렸다. 내가 평소에는 깨어있는 중간에 낮잠을 안자는 스타일인데, 어제는 점심에 낮잠까지 자버렸다. 어지간히 부트캠프에 쏟는 체력이 내 생각보다 상당한 것 같다는 생각이 든다.
그나저나 부트캠프의 강의는 생각보다 나에게 더 도움이 되고 있는 것 같다. 분명히 나는 자바를 해본 경험이 있지만, 기초가 단단하지 않고 어설프게 알고만 있는 스타일이라서 여기서 단단하게 알 수 있었으면 했는데 오늘 배운 강의 내용을 바탕으로, 조원들끼리 문제도 내고 맞춰보고 토의도 해보고 공부하는 과정속에서 내가 스스로 발전하고 배워가는게 많다고 느껴진다.
역시나 배움이 부족하다고 느껴지는 한주였던거 같기도하다. 나름 꿀리지 않는다고 생각했는데 아니었다. 더 낮추고 겸손하게 배워야 한다고 생각이 들었다. 수준높은 친구들도 굉장히 많이 보이고, 현업에서 볼법한 지식의 양을 가진 분들도 존재하였다. 역시나 LG 부트캠프에 합격해서 온 사람들 중에서는 개발이라는 큰 관점에서 실력적으로 뛰어난 분들이 오신거 같아서 나 또한 많이 자극받고 발전할 수 있을거 같다는 생각이 든다.
(나도 실력적으로 뛰어난 사람이 되고 싶다.)
진도가 조금 빨라서 따라가기가 생각보다 벅찬점과, 하루 배웠던 내용을 정리해서 블로그에 올리는게 쉽지 않은 것만 제외하면 다 좋은거 같다. 시간이 조금더 지나면 프로젝트를 하는 과정까지 추가될텐데 어서빨리 체력적으로 버틸 수 있도록 준비해야된다고 느낀다. 현재 체력으로는 버티기가 조금 어려울지도? 모르겠다.
꾸준히 진행중인 런닝을 하는 과정속에 문제가 살짝 생겼다. 런닝을 한지 얼마되지 않아서 관련지식이 많이 부족한 상태였는데, 그냥 평소에 신고다니던 신발로 뛰어댕겼더니 아킬레스건부터 발 뒷꿈치가 아파오기 시작했다. 그냥 걷기만 해도 통증이 생겨버렸다. 결국에 저번주 주말에 본가에 다녀와서 런닝화를 따로 챙겨왔고, 한동안 런닝화를 신고 뛰어야 겠다. 장비빨이 필요할거라고 생각을 못했는데 무식하면 몸이 고생한다고, 나의 패착이다.
(내 발 뒷꿈치가 지금 살려달라고 비명을 지르는거 같다.)
아 그리고, 드디어 무한도전 사진 박물관 프로젝트를 재 진행중이다. UI 개선이 이루어지고 잇고 이번주에는 어느정도 윤곽을 잡아서 라이브 서버에 배포가 가능할거 같다. 근 한달정도 신경을 못쓰고 어쩌다 보니 방치가 되어있었는데 드디어 먼지를 뒤집어 쓰고있는 이 프로젝트를 다시 가동할 떄가 된거같다. 이쁘게 단장도 하고 홍보도 해볼까 생각중이다. 조금 아쉬운건 한달정도 방치해둔것 때문에 구글에 검색하면 1순위로 결과가 노출되던 사이트가, 현재는 검색을 해도 조회가 되지 않는다. 열과 성을 다해서 신경썼어야 했는데 너무나 아쉽다.
다른 이야기로 넘어가면 요즘에 여자친구랑 집 근처 피시방 도장깨기를 시작해보고 있다. 둘다 게임하는걸 좋아하고 집에서도 게임을 충분히 즐길수 있지만 묘하게 피시방에서 하는 게임이 더 재밌다고 느끼는 타입이라 설을 맞이하여 피시방 투어를 하고있다. 오늘 다녀온 곳은 본스 피시방이었는데 사진을 찍어올걸 그랬다. 내부도 깔끔하고 괜찮았는데 블로그에 쓸 주제가 될거라고 생각을 못해서 안찍은게 아쉽다. 다음에는 사진을 꼭 찍어놔야지 나중에 피시방 갈일이 생기면 블로그 글을보고 데이터 비교해서 가야겠다 쿠쿠. 좀더 깔끔하고 좋은 곳으로 가고싶다.
이번 설이 굉장히 길어서 뭘 해도 할 수 있을거 같은데 벌써 3일이나 연휴가 지났다. 남은 연휴는 오늘을 제외하고 6일인데 어영부영 보내지 말고 좀 더 시간을 의미있게 쓸 수 있도록 노력해야 겠다. 아 오랜만에 글쓰니까 또 말이 뒤죽박죽 된거 같은데 여기서 마무리나 해야겠다.
오늘은?
(오케이, 거기까지.)
-
[lg-eureka] 부트 캠프 4일차
intro : 부트캠프 4일차 내용 정리 및 기록.
수업 내용 기록
static 키워드
static 변수는 클래스가 로딩시에 초기화 되며, 클래스의 모든 객체가 공유한다. 인스턴스 변수와는 달리 객체에 속하지 않고 클래스에 속하는 변수이다. 클래스가 로드될 때 초기화되고, 프로그램 종료시까지 메모리에 유지된다.
static 변수예제
class Example {
static int count = 0; // static 변수
public Example() {
count++; // 모든 객체가 공유
}
}
public class Main {
public static void main(String[] args) {
Example obj1 = new Example();
Example obj2 = new Example();
Example obj3 = new Example();
System.out.println("Count: " + Example.count); // 출력: Count: 3
}
}
static 메서드
클래스 레벨에서 호출되는 메서드로 인스턴스화 없이 접근 가능하다. 인스턴스 변수나 메서드를 직접 접근 가능하다.
class MathUtil {
static int add(int a, int b) {
return a + b; // static 메서드는 인스턴스 없이 호출 가능
}
}
public class Main {
public static void main(String[] args) {
System.out.println(MathUtil.add(5, 10)); // 출력: 15
}
}
final 키워드
static final 변수 (상수)
final 변수는 한번 초기화되면 값을 변경할 수 없는 상수가 된다. 특징으로는 변수선언시 즉시 초기화 하거나, 생성자에서 초기화 가능하다. 또한 한번 할당된 값은 다른 값으로 변경할 수 없다. 보통 대문자 이름과 언더스코어로 작성된다.
public class FinalVariableExample {
public static void main(String[] args) {
final int constantValue = 10; // 초기화
System.out.println("Constant: " + constantValue);
// constantValue = 20; // 오류: final 변수는 값을 변경할 수 없음
}
}
static final 메서드
가장 큰 특징으로는 서브클래스에서 재정의 (오버라이딩)이 불가하다, 이렇게 메서드에 final을 사용하는 이유는 메서드의 구현을 고정해서 상속받는 클래스에서 변경하지 못하도록 보호에 목적이 있다.
class Parent {
public final void display() {
System.out.println("This is a final method.");
}
}
class Child extends Parent {
// @Override
// public void display() { // 오류: final 메서드는 오버라이딩 불가
// System.out.println("Cannot override final method.");
// }
}
접근 제한자
public : 어디에서든 접근 가능.
protected : 같은 패키지 + 하위 클래스에서만 접근 가능 (상속관계)
default : 같은 패키지에서만 접근 가능
private : 클래스 내부에서만 접근 가능
(Java의 접근 제한자)
가장 헷갈리는 protected와 default 에제로 이해해보기
protected 키워드 예제 (상속관계가 포인트)
// package1/Parent.java
package package1;
public class Parent {
protected void display() {
System.out.println("Protected method");
}
}
// 같은 패키지
package package1;
public class SamePackage {
public void test() {
Parent parent = new Parent();
parent.display(); // 같은 패키지에서 접근 가능
}
}
// 다른 패키지 - 상속 관계
package package2;
import package1.Parent;
public class Child extends Parent {
public void test() {
display(); // 하위 클래스에서 접근 가능
}
}
// 다른 패키지 - 상속 관계가 아님
package package2;
import package1.Parent;
public class NonChild {
public void test() {
Parent parent = new Parent();
// parent.display(); // 오류: 상속 관계가 아니므로 접근 불가
}
}
default 키워드 예제 (같은 패키지가 포인트)
// package1/Example.java
package package1;
class Example { // default 접근 제한자
void display() {
System.out.println("Default method");
}
}
// 같은 패키지
package package1;
public class Main {
public static void main(String[] args) {
Example example = new Example();
example.display(); // 같은 패키지에서 접근 가능
}
}
// 다른 패키지
package package2;
import package1.Example;
public class Main {
public static void main(String[] args) {
// Example example = new Example(); // 오류: 다른 패키지에서는 접근 불가
}
}
상속
상속은 Java의 객체지향프로그래밍에서 핵심 개념 중 하나로, 기존 클래스의 멤버변수와 메서드를 자식 클래스가 물려받아 사용하는 것을 의미한다. 상속은 코드 재사용성을 높이고 계층적인 구조를 통해 코드의 논리적 설계를 용이하게 만든다. 다만 Java에서는 단일 상속만을 허용하고, 결합도가 높아지는 단점이 존재한다.
아래 코드를 보면, Child는 Parent 클래스를 상속받는다. Child 클래스는 diplay 메서드가 존재하지 않지만, 상속을 통해 상위 클래스의 메서드를 물려받아 실행 할 수 있다.
class Parent {
int value = 10;
void display() {
System.out.println("Parent value: " + value);
}
}
class Child extends Parent {
void show() {
System.out.println("Child value: " + value);
}
}
public class Main {
public static void main(String[] args) {
Child child = new Child();
child.display(); // 부모 클래스의 메서드 호출
child.show(); // 자식 클래스의 메서드 호출
}
}
추가적으로 상속에서는 상위 클래스에 기본생성자가 아닌, 매개변수가 있는 생성자가 존재하는 경우 해당 클래스를 상속받는 클래스에서 상위 클래스의 생성자를 호출해주어야 한다. 해당 내용과 관련된 예제 코드는 다음과 같다.
class Parent {
int value;
// 매개변수가 있는 생성자
Parent(int value) {
this.value = value;
System.out.println("Parent Constructor called with value: " + value);
}
}
class Child extends Parent {
int childValue;
// 자식 클래스 생성자
Child(int value, int childValue) {
super(value); // 명시적으로 부모 클래스의 생성자를 호출
this.childValue = childValue;
System.out.println("Child Constructor called with childValue: " + childValue);
}
}
public class Main {
public static void main(String[] args) {
Child child = new Child(10, 20);
}
}
상속에 대한 개념을 알아가면서 오버라이딩(overriding) 개념이 더 부각되게 된다. 부모 클래스의 메서드를 자식 클래스에서 재정의하여 다른 동작을 수행하도록 하는 것을 말한다. 다만 부모 메서드와 동일한 이름, 반환타입, 매개변수를 가져야 한다.
다음 예시는 부모 클래스의 메서드를 자식 클래스에서 재정의 하는 예시이다.
class Parent {
void display() {
System.out.println("This is the parent class method.");
}
}
class Child extends Parent {
@Override
void display() {
System.out.println("This is the overridden method in the child class.");
}
}
public class Main {
public static void main(String[] args) {
Parent parent = new Parent();
parent.display();
Child child = new Child();
child.display();
Parent polymorphic = new Child();
polymorphic.display(); // 다형성
}
}
// 출력문 결과
This is the parent class method.
This is the overridden method in the child class.
This is the overridden method in the child class.
위 코드에서 실행결과는 결과는 재정의된 메서드가 실행됨에 따라 Child가 인스턴스화된 객체는
This is the overridden method in the child class. 가 출력된다.
-
[lg-eureka] 부트 캠프 3일차
intro : 부트캠프 3일차 내용 정리 및 기록.
수업 내용 기록
데이터 타입 [기본 타입]
기본형 타입인 byte, short, int, long, float, double, char, boolean 등의 데이터 타입을 말한다. 기본 타입으로 선언된 변수는 값 자체를 저장하고 있지만, 참조 타입으로 선언된 변수는 객체가 생성된 번지 즉 주소값을 보유하고 있다.
데이터 타입 [참조 타입]
객체의 번지를 참조하는 타입, 배열, 열거, 클래스, 인터페이스 같은 것들을 말한다. 기본 타입으로 선언된 변수는 값 자체를 저장하지만, 참조 타입으로 선언된 변수는 객체가 생성된 메모리 번지를 저장한다.
JVM의 힙, 스택, 메소드 영역
JVM은 운영체제에서 할당받은 메모리 영역을 힙 영역, 스택 영역, 메소드 영역으로 구분해서 사용, 벤더사마다 JVM 구조는 디테일 하게는 다르지만 큰 관점에서 공통된 부분은 존재함.
(JVM 예시 이미지)
힙 영역
객체가 생성되는 영역, 객체의 번지는 메소드 영역과 스택 영역의 상수와 변수에서 참조, 결론적으로 객체와 배열을 저장하는 공간.
스택 영역
메소드를 호출할때마다 생성되는 프레임이 저장되는 영역, 메서드 호출과 관련된 로컬 변수 및 참조 변수를 저장.
메소드 영역
클래스 영역이라고도 부르기도한다, 클래스의 메타데이터, 정적 변수(static 변수), 상수(Constant Pool), 그리고 메서드 코드(바이트코드)가 저장됨.
String 타입
String Constant Pool: 리터럴 문자열(“example”)은 JVM의 String Constant Pool에 저장되어 메모리를 절약. 동일한 리터럴이 있을 경우 Pool의 객체를 재사용.
String s1 = "Hello";
String s2 = "Hello"; // s1과 s2는 같은 객체를 참조
new String()을 사용하면 새로운 String 객체가 Heap에 생성된다.
String s1 = new String("Hello"); // Heap에 새 객체 생성
값 목록으로 배열 생성
다음과 같이 값 목록으로 배열을 생성할 수 있다.
int[] arr = {1, 2, 3, 4, 5};
int[] arr = new int[] {1, 2, 3, 4, 5};
다만 다음과 같은 방법으로 배열을 생성하는게 일반적이다.
int [] arr = new int[4];
arr[0] = 0;
arr[1] = 1;
arr[2] = 2;
arr[3] = 3;
객체의 개념과 관계
객체란 물리적으로 존재하거나 개념적인 것 중에서 다른 것과 식별 가능한 것, 객체는 속성과 동작으로 구성. 자바는 이러한 속성과 동작을 각각 필드와 메소드라고도 말함.
집합관계 : 완성품과 부품의 관계
사용관계 : 다른 객체의 필드를 읽고 변경하거나 메소드를 호출하는 관계
상속관계 : 부모와 자식 관계, 필드 메소드를 물려받음
객체지향 프로그래밍의 특징
[캡슐화]
객체의 데이터(필드)와 행동(메서드)를 하나로 묶고, 외부에서 객체의 내부 구현 세부사항에 접근하지 못하도록 하는 것.
[상속]
부모 클래스(슈퍼 클래스)의 필드와 메서드를 자식 클래스(서브 클래스)가 물려받아 사용하는 것.
[다형성]
같은 형태의 코드(메서드 호출)가 다양한 실행 결과를 만들어내는 것.
[추상화]
복잡한 현실 세계의 개념을 필요한 속성과 행동만 포함하여 프로그래밍적으로 표현하는 것.
클래스와 인스턴스
객체 지향 프로그래밍에서도 객체를 생성하려면 설계도에 해당하는 클래스가 필요함. 클래스로부터 생성된 객체를 해당 클래스의 인스턴스라고 부르며, 클래스로부터 객체를 만드는 과정을 인스턴스화 라고 함, 또한 동일한 클래스로부터 여러개의 인스턴스를 만들 수 있음
this() this의 차이
this()는 생성자에서 다른 생성자를 호출할 때 사용. 동일한 클래스 내에 있는 다른 생성자를 호출하여 코드 중복을 줄이고 객체 초기화를 간결하게 만든다. 주의할 점으로 생성자의 첫 번째 줄에서만 호출할 수 있고, 생성자 체이닝에서 최종적으로는 자기 자신의 초기화가 이루어져야 한다.
this는 현재 객체 자신을 참조하는 키워드. 같은 클래스 내부에서 필드 메서드 생성자 등을 명시적으로 호출하거나 외부에서 전달된 변수와 구분 할때 사용한다.
4조 workshop 토의 내용 기록
메서드 시그니처
메서드 시그니처란 메서드를 고유하게 식별하는 요소를 말한다. 주로 메서드 이름과 매개변수 리스트로 구성되며, 컴파일러가 오버로딩과 오버라이딩을 구분할때 사용한다. 메서드 시그니처의 구성 요소로는 메서드 이름과, 매개변수 리스트(매개변수의 타입, 순서, 개수)를 포함한다. 주의할 점으로 반환타입은 메서드 시그니처에 포함되지 않는다.
객체지향과 절차지향의 차이
[OOP]
현실세계의 객체를 모델링하여 데이터를 객체로 묶어 처리한다. 코드 재사용성이 높고 유지보수성 및 확장성에 장점이 있다.
[POP]
작업을 순차적으로 처리하며 함수 중심으로 설계한다. 특징으로는 데이터와 함수를 분리하는데 단순하여 빠르게 구현 가능함에 장점이 있다.
중요한 둘의 차이점으로는 객체지향은 객체 중심, 절차 지향은 함수 중심에 있다.
String은 언제 heap과 String Constant Pool에 저장될까?
기본적으로 리터럴을 사용한 문자열 String은 String Constant Pool에 저장된다. 만약 new 연산자를 통해 String 객체를 생성하는 경우는 heap 영역에 생성되게 된다. 위 개념은 아래 코드에서 비교연산자 == 를 통해 주소값을 비교할때 true false 값의 결과가 달라진다.
public class Main {
public static void main(String[] args) {
String str = "hello"; // String constant pool에 저장
String str2 = "hello"; // String constant pool에서 재사용
String str3 = new String("hello"); // 별도의 Heap 메모리에 저장
System.out.println(str == str2); // true (같은 객체를 재사용하기 때문에)
System.out.println(str == str3); // false
System.out.println(str.equals(str3)); // true
}
}
위 과정에서 str과 str2는 리터럴값이 같은 문자열이기에 내부적으로 같은 주소값(String Constant Pool)을 할당하게 된다. 그렇기에 str과 str2은 true 값을 반환하며, new 연산자로 생성한 str3는 heap 영역에 할당되게 되어, 새로운 주소값이 할당된 값을 보유하고 있기에 str과 str3은 false 값을 반환하게 된다.
(Heap과 String Constant Pool 영역)
-
[lg-eureka] 부트 캠프 2일차
intro : 부트캠프 2일차 내용 정리 및 기록.
수업 내용 기록
오버 플로우, 언더플로우
// 정수
public class Test {
public static void main(String[] args) {
int a = 128;
byte b = (byte) a;
System.out.println("b = " + b); // -128 (오버 플로우)
a = -129;
b = (byte) a;
System.out.println("b = " + b); // 127 (언더 플로우)
}
}
// 실수
public class Test {
public static void main(String[] args) {
// 오버플로우 예제
float floatMax = Float.MAX_VALUE; // 3.4028235E38
System.out.println("Float MAX: " + floatMax);
float floatOverflow = floatMax * 2; // 오버플로우
System.out.println("Float Overflow: " + floatOverflow); // Infinity
double doubleMax = Double.MAX_VALUE; // 1.7976931348623157E308
System.out.println("Double MAX: " + doubleMax);
double doubleOverflow = doubleMax * 2; // 오버플로우
System.out.println("Double Overflow: " + doubleOverflow); // Infinity
// 언더플로우 예제
float floatMin = Float.MIN_VALUE; // 1.4E-45 (가장 작은 양수)
System.out.println("Float MIN (Positive): " + floatMin);
float floatUnderflow = floatMin / 2; // 언더플로우
System.out.println("Float Underflow: " + floatUnderflow); // 0.0
double doubleMin = Double.MIN_VALUE; // 4.9E-324 (가장 작은 양수)
System.out.println("Double MIN (Positive): " + doubleMin);
double doubleUnderflow = doubleMin / 2; // 언더플로우
System.out.println("Double Underflow: " + doubleUnderflow); // 0.0
}
}
Float MAX: 3.4028235E38
Float Overflow: Infinity
Double MAX: 1.7976931348623157E308
Double Overflow: Infinity
Float MIN (Positive): 1.4E-45
Float Underflow: 0.0
Double MIN (Positive): 4.9E-324
Double Underflow: 0.0
정수 연산
산술 연산을 정확하게 계산 하려면 실수 타입을 사용하지 않는 것이 좋음. 정확한 계산이 필요하면 정수 연산으로 변경 (double, float으로 정확한 실수 연산을 하는건 비추천, Math Class의 BigDecimal을 이용하는게 좋음)
나눗셈 연산에서 예외 방지하기
나눗셈 또는 나머지 연산에서 좌측 피연산자가 정수이고 우측 피연산자가 0일 경우 ArithmeticException 발생, 좌측 피연산자가 실수이거나 우측 피연산자가 0.0 또는 0.0f이면 예외가 발생하지 않고 연산의 결과는 무한대 또는 NaN이 됨
비트 이동 연산자
<< (왼쪽 시프트)
• 모든 비트(부호 비트 포함)를 왼쪽으로 이동.
• 오른쪽 빈자리는 항상 0으로 채움.
>> (부호 있는 오른쪽 시프트)
• 모든 비트를 오른쪽으로 이동, 부호 비트는 이동하지 않음.
• 왼쪽 빈자리는 부호 비트 값으로 채움 (1 또는 0).
>>> (부호 없는 오른쪽 시프트)
• 모든 비트를 오른쪽으로 이동, 부호 비트 포함.
• 왼쪽 빈자리는 항상 0으로 채움.
4조 workshop 토의 내용 기록
논의 1. 쇼트서킷이 뭘까? (feat. && || & |)
쇼트서킷이란? 논리연산자에서 좌측 피연산자 만으로도 결과가 확정된 경우, 굳이 우측 피 연산자의 계산 과정을 진행하지 않는 기능이다. 쇼트서킷의 연산 과정을 통해 불필요한 연산을 생략함으로써 성능적으로 이점을 볼 수 있다. 자바에서 || 이 대표적으로 쇼트서킷을 활용할수 있는 논리 연산자로 볼 수 있는데, 다음과 같은 예시에서 쇼트 서킷이 뭔지 알 수 있다.
public class Test {
public static void main(String[] args) {
int x = 10;
int y = 20;
x++; // x = 11
y--; // y = 19
if (x == 11 || y == 20) {
System.out.println("쇼트서킷을 통해 y값이 19이지만, 조건식의 연산이 최종적으로 true로 연산되어 출력문이 출력된다.");
} else {
System.out.println("쇼트서킷 실패");
}
}
}
그렇다면 비트 연산자인 &와 | 는 왜 쇼트서킷이 적용이 안될까? 그건 바로 비트 연산자는 좌항과 우항의 자릿수의 비트에 대한 비트연산을 적용하는 방식으로 동작하기 떄문이다.
0101 (a)
& 0011 (b)
------
0001 (결과: 1)
===================
0101 (a)
| 0011 (b)
------
0111 (결과: 7)
논의 2. yeild는 어디에 사용하는 키워드일까?
확인문제 2번의 정답 중 yield(반환하다, 돌려주다) 라는 키워드에 대한 논의를 진행하였다. 다음과 같은 예시에서 yield의 사용법을 엿볼수 있었는데, 간단하게 설명해보자면 switch case문에서 결과값을 반환할 때 사용하는 키워드 값이라고 볼 수 있다.
함수에 값을 돌려준다는 개념으로 접근해야하는데, return은 메서드를 종료하며 값을 반환하는 것이지만, yield는 switch case 문의 값을 시작하는 지점으로 값을 돌려준다.
public class Test {
public static void main(String[] args) {
String grade = "B";
int score = switch (grade) {
case "A" -> 100; // 단순 리터럴 반환
case "B" -> {
int deduction = 20; // 블록 내에서 연산 수행
int result = 100 - deduction;
yield result; // 연산 결과 반환
}
default -> 60; // 기본값 반환, yeild 사용시에 defult 필수
};
System.out.println(score); // 출력: 80
}
}
화살표(->)를 사용하여 단순 값을 반환할 때는 yield 없이 바로 값을 지정할 수 있다.
논의3. printf는 뭘까?
정수를 표현하여 출력할떄는 %d, 실수를 표현하여 출력할때는 %f, 문자를 표현하여 출력할때는 %s, 시간을 표현하여 출력하는 %t, 결국 printf의 사용은 지시자를 통해 변수의 값을 표현하는 개념이 적용된다.
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 6; j++) {
// 값 출력시에, i,j의 값을 정수값으로 표현해주는 서식
// %d: 정수, %s: 문자열 %t : 시간 등등
System.out.printf("%d %d ", i, j);
}
System.out.println();
}
}
}
논의4. 소수점 표현에 대한 이야기
소수를 표현하는 타입에는 float 과 double 이 있지만 정밀도의 문제로 float은 7자리까지, double은 15자리까지 표현된다(유효숫자). 하지만 정확한 소수관련된 데이터를 다루기 위해서는 Bigdecimal 클래스를 사용한 로직을 구성해야 정확한 계산을 진행할 수 있다. 왜냐하면 float과 double은 소수를 이진수로 표현하는 과정에서 대부분 무한 반복 소수가 되어 근사치 값으로 저장되기 때문이다.
예를들어 0.1을 2진수로 표현하려면 어떻게 표현되는지 아래 예시를 통해 알아보자.
0.1 * 2 = 0.2 (정수 부분은 2진수로 기록, 소수 부분은 다음 계산에 사용)
위 과정을 반복.... 아래와 같은 연산이 반복됨
위 과정의 반복이 종료되려면, 결과값이 0이 되어야 종료.
0.1 * 2 = 0.2 → 정수 부분 : 0
0.2 * 2 = 0.4 → 정수 부분 : 0
0.4 * 2 = 0.8 → 정수 부분 : 0
0.8 * 2 = 1.6 → 정수 부분 : 1
0.6 * 2 = 1.2 → 정수 부분 : 1
결론적으로 0.1을 2진수로 표현하려면 0.0001100...(무한반복)
0.1은 2진수로 정확히 표현되지 않고, 무한 반복 소수가 된다.
결론적으로 컴퓨터는 위의 무한반복되는 값을 근사값으로 저장한다.
Bigdecimal에 대한 사용을 진행하면 내부적으로 Integer 끼리의 연산을 진행하고 차후 마지막 결과값을 반환시에 어느 부분에서 소수점을 찍어야 하는지 처리되기때문에 정확한 연산이 가능하게 된다.
위와 같은 장점이 존재하지만, 단점으로는 많은 메모리와 오버헤드를 유발하기에 대량의 숫자 연산을 처리할 때 문제가 될 수 있다.
-
[diary] 현 시점 온 우주의 기운이 돕는 사람
intro : 정말 오랜만이지?
글 마지막 작성이 거의 10일 이상되었다. 마지막 글작성일자가 지금보니, 1월4일인걸 보니까 자그마치 14일이네? 이렇게나 오랬동안 글을 안쓴적이 있었던가? 기억을 더듬어봐도, 깃허브 블로그를 시작하고 나서 단 한번도 그랬던 적이 없었던것 같다.
왜냐하면 LG 유플러스 유레카 부트캠프에 대한 면접을 보고나서, 면접 결과를 기다리는 과정속에서의 시간을 좀 처럼 다른것에 집중하면서 시간을 보낼 수 없었기에 기존에 하던 모든 루틴을 내려두고 결과가 빨리 나오기만을 기다리고 또 합격을 기도하였다.
나름 내 스스로 간절했다고 생각을 못했었는데 막상 면접을 보고 결과를 기다리는 과정속에서 생각보다 내가 이 부트캠프에 대한 열망의 간절함이 있다는걸 역설적이게도 느낄수 있었다. 입으로는 떨어져도 괜찮다 라고 주변에 말하고 다녔지면 돌이켜 보면, 진짜 내마음은 붙기를 간절히 바랬던거 같다.
(생각보다 나 간절했구나?)
일단 면접에 대한 이야기부터 좀 해봐야 할거 같다. 지금까지 살아온 인생에서의 면접이라곤 딸랑 2-3번정도의 면접 경험만 존재하는데, 앞선 면접에 대한 이야기또한 이미 윗선에서의 이야기가 끝낸? 교수님을 통해 취업을 하는 경우나, 주변 지인의 소개로 인한 면접이기에 사실상 절차상의 면접에 대한 경험만 존재하기에 심도있는 면접은 단 한번도 해본적이 없었다.
그렇기에, 이번 유레카 면접은 정말로 공채면접을 보듯이 준비를 해야했는데 해본적이 없었기에 준비또한 쉽지가 않았던것 같다. 예를들면 자기소개는 어떻게 해야하는가? 혹은 기술질문에 대한 답변은 어떻게 해야하는가? 정답이 없는 문제에 대해서 나의 가치를 증명할수 있는 답변이 필요했기에 더욱 어려운 문제라고 생각했었따.
마치 질문에 질문이 꼬리를 물고 답변할수 있을정도로 준비를 했었는데, 기술질문에 대한 답변은 기존에 내가 경험했던 실무에서의 경험을 바탕으로 답변하면 오히려 쉽게 풀리겠다고도 생각했다. 실제로 기술질문은 내가 알던 상식선에서 답변을 충분히 할 수 있었던 것 같다. 나름 준비에 대한 보람이 느껴지는 순간이었다.
다만, 자기소개는 생각보다 준비했던것보다 잘 못해서 많이 아쉬웠었다. 만약 다른 기업을 가기위한 준비를 해야하는 상황이 있다면 기술질문에 대한 답변보다, 자기소개를 더 열심히 준비해야겠다고 생각이 들정도다.
세상에 나쁜경험은 없다고 하던가? 정말 이번 면접준비를 하면서 얻어가고 배워가는게 많은것 같다. 자세한 이야기는 대외비이기에 이야기 할 수 없기에 여기까지만 언급해야겠다.
(요기까디.)
그렇게 면접은 화요일에 진행되었었고, 수요일 목요일은 금요일의 결과를 기다리는 시간이었는데 그동안 아무것도 하지못하고 붙을지 떨어질지만 점쳐봤던거 같다. 시간이 흘러 금요일이 되었고 연락을 받게되는데. 정말 심장이 덜컥했었다.
(아.)
아무래도 코딩테스트 문제를 많이 맞추지 못했던 상황이라서 면접을 아무리 잘봐도 좀 어려울수도 있겠다 싶긴 했었었는데, 다행이면서도 아쉬운 결과로 추가합격문자를 받았다. 사실 여기서 많이 포기했었다. 추가합격이 나려면 기존에 합격자 분들중에서 수강을 포기해야하며, 내가 예비번호가 배치된것이 앞 번호로 배치가 되어있어야 나에게 연락이 올 수 있는 상황이라서 사실상 이건 추합 확률이 낮다고 판단을 했었다.
여자친구랑도 아무래도 LG 유플러스 유레카 부트캠프를 놓아주어야 할 것 같다고 이야기하면서 기존의 루틴으로 돌아가려고 부단히 노력하였다. 한 일주일 정도에서 10일 정도를 면접을 준비하는 시간과, 결과를 기다리는 시간으로 소비하고 나니 뇌가 굳어서 뭔갈 하기가 쉽지 않았다. 그래서 남는 시간에 운동이나 게임쪽으로 관심사를 바꿔보려고 했었다.
면접 결과 발표 주 다음에는 추가합격 발표주가 기다리고 있었기에 일말의 기대감은 없었지만 사람 마음이라는게 아무래도 희망을 품기마련이기에 이 또한 신경이 안쓰일수가 없었다. 그렇게 시간이 흘러 1월 14일 화요일 아침이었다. 오늘도 어김없이 9시즈음에 일어나서 스트레칭을 하고 있었는데, 갑자기 전화가 왔다. 근데 아이폰의 다이나믹 아일랜드에 멀티캠퍼스 라는 단어가 보였다. 순간적으로 아 이거 추가 합격 전화다 라는 생각이 들었고 바로 전화를 받았다.
역시나 예상대로 추가합격 안내전화였고 나는 소리는 지르지 못하였지만 얼마나 기뻣는지 모른다. 그렇게 나는 기적적으로 LG 유플러스 유레카 부트캠프에 합류하게 되었다.
(쑤아리질러~~~~~)
전화를 받고나서 문자와 메일로 최종합격에 대한 재 안내가 이루어졌는데 진짜 이걸보니까 실감이 나기 시작했다. 추합이라는 기적이 이루어지는구나, 특히나 비대면으로 신청했었기 때문에 더욱 인원이 빠지지 않을거라고 생각했었는데 온 우주가 날 돕는다고 느껴졌다.
(진짜 합격이구나.)
정말 그동안 글을 쓰지 못한 기간동안에 일어난 일들에 대해서 요약된 기록을 이게 전부인거 같다.
이제는 물 흐르듯 짜여진 시간 속에서 열심히 공부할 일만 남은 것 같다. 혼자 준비하고 기초를 닦는 과정도 나쁘지 않다고는 생각했지만, 그보다 더 좋은 환경이 주어지는 것이기에 앞으로 어떻게 더 열정적으로 임하고 배울 수 있을지 고민이 된다. 다들 잘하는 인원들로만 부캠이 구성되어 있을 거라고 생각하니 열정이 솟는다. 기존에 1기 최우수 작품을 깃허브에서 찾아봤는데, 퀄리티가 굉장히 높았다. 저 정도 수준이 된다면 앞으로 나의 커리어에 반드시 도움이 될 거라는 생각이 들어서 흥분이 된다.
(찐 광기를 보여주지.)
이런거에 흥분하는 변태가 아니었는데 점점 이런거에 흥분되는 사람이 되어버렸다. 잘하는사람들과 할 수 있는 기회는 좀처럼 없었기에 시너지가 얼마나 날지도 궁금하다.
부트캠프에 대한 이야기는 여기까지 언급하도록 하겠다.
그동안 진행하지못했던 무한도전 사진 박물관 프로젝트는 주말에만 진행해야할거 같다. 2주동안 해뒀어야 했는데 도저히 집중이 안되어서 못한게 너무 아쉽다. 심지어 내 깃허브 잔디를 보면 내가 얼마나 마음 졸였는지 알 수 있는데, 어지간하면 매일매일 커밋을 찍는 나였는데 그냥 연속 3일동안 커밋을 안찍은 날도 존재한다.
(그래도 하루 1커밋은 했어야 했는데. 후.)
백준 문제도 안풀어서 골드5에서 주차해놓고 멈춰둔것도 좀 아쉽다. 그래도 한두문제씩은 꾸준히 해야 감을 잃지 않는건데, 멘탈적으로 내가 너무 약한가? 싶기도 하다. 심지어 무한도전 사진박물관 프로젝트는 그냥 AWS에 서버 켜두고 내비두기만 해서 뭐 제대로 하고 있지도 않는데 돈만 나가게 생겼다.
(VPC 돈나가는건 진짜 킹받네.)
아 몰라. 주말부터 열심히 해서 코드 수정도 하고 개발도 이제는 진짜 다시 시작해야 한다. 계획했던 걸 드디어 다시 톱니바퀴를 굴릴 수 있을 것 같다. 얼른 관리자 페이지 구성을 손좀 봐서 이미지 카테고리별로 분류해서 사용할 수 있도록 해야지. 나의 첫 번째 프로젝트를 이렇게 마무리할 수는 없다.
다음 주 평일에 글을 쓸 수 있을지는 모르겠지만, 블로그에 신경을 다시 많이 쓸 수 있도록 노력해야겠다. 오늘은 글을 여기까지 쓰고 얼른 뛰러 가야겠다. 답답한게 찬바람 맞으면서 숨 헐떡거리며 뛰면 기분이 그렇게 좋을 수 없다. 점차적으로 뛸 수 있는 시간도 늘려봐야지.
오늘의 기록은 여기까지.
(준하형도 요즘 얼굴 보기만해도 웃음벨이네.)
-
-
-
[diary] 25년도 새 출발을 알리면서
intro : 25년도에는 건강하고 행복한 한해를 보내기를.
원래 이번주 토요일에 글을 쓰려다가 이번주 토요일에는 면접 준비를 해야할거 같아서 1월1일 기념을 하기도 하고, 어차피 글 쓸 시간이 없을거 같으니, 미리 이번주의 기록을 남기고자 한다. 갑자기 웬 면접 이야기냐고?
(??????)
10문제 중에 3문제 푼 내가? 이게 된다고? 진짜 면접보러가?
(공부와 댐을 쌓고 살아도 솟아날 구멍이 있구나!)
그렇다. 3차면접. 마지막 이 면접만 통과하면 부트캠프를 참여할 수 있다. 이번년도 가장 큰 도전이 되겠지만 꼭 참여하고 싶다. 그래서 주말에는 면접 준비를 좀 해야할거 같다. 저번 글에서도 알아봤던 후기글에서 가장 중요한게 면접이라고 하였으니, 얼마나 내가 이 부트캠프에 참여하고 싶은지, 이 부트캠프를 통해 무엇을 얻어가고 싶은지 등등 다양한 질문에 대해 내 스스로 답을 낼 수 있는 시간이 필요할 것 같다.
이미 면접에 대한 컨셉은 내 스스로 좀 정해둔게 있는데, 가감없는 솔직함을 컨셉으로 들고 갈 생각이다. 뭐 꾸밀것도 거짓말을 할것도 없는 나의 진실된 모습을 보여주는게 가장 좋은 방법이 되지 않을까 하는 생각을 한다. 예상질문이라는게 준비한대로 나오면 좋겠지만 이상하게 내가 준비한 것들은 꼭 실전에선 안나오는게 국룰같아서 기존에 내가 겪었던 경험들에 대해서 솔직하게 대답할 준비를 잘 해야겠다. 다만 감정이 실려서 욕만 하지 않게 조심해야겠다.
(얘기하다보면 전에 다니던 회사욕이 자연스럽게 나올거 같아서 참아야 할거 같다.)
후, 면접 이야기는 여기까지이고 무한도전 사진 박물관 프로젝트가 진행이 좀 많이 되었다. 드디어 클립보드로 이미지 복사도 되고 쉽게 이미지 다운로드도 된다. ec2로 인프라 구조를 변경한게 아주 신의 한수 였다.
(지금 사용하는 무도짤은 다 저기서 가져오고 있다. 아주 좋다.)
다만 아쉬운 점은 아이폰 모바일 에서는 아쉽게도 클립보드로 이미지 복사 기능이 동작하지 않는다. 다행히 이미지 다운로드는 동작을 하긴 하는데 좀 아쉽달까… 아이폰 정책상 보안에 위협되는 항목은 전부 다 막아버리는거 같다. 갤럭시에서 테스트 할때는 동작 잘만하는데 아이폰만 그런다.
(아이폰에서 되어야 내가 편하게 쓰는데 젠장이다.)
그리고 이미지 검수 기능을 만들었다. 아무래도 크롤링한 이미지 데이터가 완벽하게 무도 짤인지는 사람 눈으로 검수가 필요해서 관리자 페이지에서 검수할 수 있도록 진행했다. 특정 인원만 관리자 페이지에 접근해서 작업할 수 있어야 하기에 로그인 기능을 구현했고 Spring Security + JWT 토큰방식을 적용했다.
(관리자 로그인 페이지)
(관리자 이미지 검수 페이지)
디자인적인 부분은 여전히 부족하지만, 틈날 때마다 내 폰으로 어디서든 손쉽게 이미지 검수를 할 수 있다는 점은 정말 편리한 것 같다. 더 깊은 이야기는 나중에 project 탭에서 다룰 예정이다. 개발 과정을 project 탭에 기록해야 하는데, 기능 개발에 몰두하다 보니 시간 가는 줄 모르겠다. 원래는 하루에 한두 개씩 글을 틈틈이 올리려고 했었는데, 요즘은 바쁘다 보니 많이 못 썼다. 이상하게 고민할 것도 많고 바쁜 날들의 연속인데, 그래도 만족스럽다. 짧은 시간 동안 디자인은 좀 아쉬워도 내가 원하는 기능은 전부 구현했으니까!
(나름 만족!)
보통 일주일만에 글을 쓰면 참 할 이야기가 많은데 아무래도 저번주 일요일부터 화요일까지의 일들을 기록하는거다보니까 여기까지가 그동안 있었던 일의 끝인거 같다. 그러면 이번년도에 뭘 이루고 싶은지나 적어봐야겠다. 목표나 세워봐야지.
음 이번년도에는 정보처리기사 자격증을 꼭 취득하고 싶다. 생각해보니 당장 준비를 시작해야할거 같다. 내용도 많고 공부는 미리미리 해둬야 한다는걸 리눅스 자격증 준비를 하면서 느꼇다. 난 벼락치기 스타일이 안맞는거 같다. 꾸준히 준비하고 시험 전날에는 그냥 푹 자버려야 다음날 컨디션이 좀 괜찮은거 같더라, 오히려 전날에 무리하면 다음날 준비를 아무리많이해도 본 실력도 안나오고 나하고 안맞는 방법인거 같다.
그리고 꼭 LG 유플러스 유레카 부트캠프에 참여하고싶다. 다른 부트캠프보다 뭔가 얻어갈게 많을거 같은 커리큘럼이고 무엇보다 꾸밈없고 단단해보이는 과정이 마음에 든다. 뭔가 사람을 꼬셔서 현혹시키려는 그런 커리큘럼이 아니라 정말 실무에 필요한 기술들만 알차게 배우게 하려고 하는거 같달까? 사실 다른 부트캠프 많이 찾아보지도 않았는데 다 뭔가 화려하기만 하고 알맹이가 없어보인다. 생성형 AI가 어쩌구. 이러는데 백엔드 기술 배우기도 바빠죽겠는데 AI 까지 도입한다니 이게 정말 6개월 안에 할 수 있는 과정인가 싶은 것들이 널리고 널렸다.
그리고 마지막으로 건강해지고, 강해지고싶다. 지금까지 살아오면서 뭔가 신체적으로 정신적으로 건강하지 못한 삶을 살았던거 같은데 강해지면 건강해질수 있고, 건강해지면 강해질수 있을거 같다. 나도 내가 지금 무슨 소리 하는지 모르겠지만 일단 운동을 꾸준히 해야겠다는 생각은 계속 든다. 계속해서 복근 운동을 하는데 살은 아직안빠지는게 좀 아쉽달까. 푸쉬업은 효과가 좋아서 팔에 힘을주면 운동한 티가 좀 나기 시작했는데, 복근은. 한참 나중에나 운동한 티가 날거 같다.
(그래그래 건강해지고 강해져서 장수와 번영을 이루는거야!)
아! 진짜 마지막으로 이번년도에 모바일 어플을 하나 개발해서 출시해보고싶다. 최근에 무한도전 사진 박물관 프로젝트를 진행하면서 얻은 자신감도 있고, 간단하게라도 배포해서 AppStore에서 내가 만든 어플을 다운받아서 사용해보고 싶다. 도전은 항상 좋은거니까. M1 맥북이 힘을 좀더 내줬으면 좋겠다. 돈벌어서 더 좋은 노트북으로 바꿔줄게. 조금만 더 힘을 내줘!
아이디어와 기획, 컨셉은 여자친구랑 계속해서 상의를 하고 있다. 기존에 일정 모듈을 만들려고 했는데 여자친구 말로는 그거 만들어도 잘 안쓸거 같다고 해서, 나도 어느정도 동감하는 부분이기에 차후 작업할 예정이었던 프로젝트의 주제를 변경해야 하나 고민중이다.
일단 기존에 진행중인 무한도전 사진 박물관 프로젝트 마무리를 하고나서 천천히 어떤 주제로 진행할지 좀 더 깊게 생각을 해봐야겠다. 아마 이번달 1월 안에 무한도전 사진박물관 프로젝트는 대충 마무리가 될거 같다. 부트캠프 시작전에는 마무리 해야 부캠 시작하고나서는 공부에만 집중할 수 있을테니까 강제로라도 대충 마무리를 지어야 할거 같다.
이말은 즉슨 아마 이번달은 올라오는 글의 양이 좀 적을 수 있을거 같다. 기능개발에 힘을 줘야 하니까.
(프로젝트는 마무리하고 부캠들어야 할거 아니냐고~)
하여튼. 오늘은 글을 여기까지 쓰는걸로 하고. 한번만 더 나에게 행운이 깃들어서 부캠 참여라는 좋은 결과를 가지고 올 수 있기를.
(럭키비키 원모어타임 플리즈)
이번주 토요일인가 금요일에 리눅스 마스터 시험결과가 나오는데 합격하면 글한번 더 쓸수도 있다. 기쁜건 나누면 두배잖아?
-
[diary] 24년도 마지막 토요일을 즐기면서
intro : 그러고보니 오늘이 24년도 토요일 마지막이네?
오늘도 어김없이 이번주의 기록을 남기고자 글을 쓴다. 이번주는 뭐 했더라 ?
월요일부터 뭘 했는지 한번 생각해보니, 이번주 초에 무한도전 프로젝트의 갤러리 탭을 하나 구성했다. 기존에 크롤링했던 이미지를 사용자에게 쉽게 보여주고 복사 및 다운로드를 할 수 있도록 하고 싶었다. 카카오톡 이모티콘처럼 누구나 내 사이트의 재밌는 짤을 가져다 쓰면 얼마나 좋을까? 라는 생각이 있었기 때문이다. 이런 아이디어는 이모지 사이트 를 보면 별의별 이모티콘을 제공해주고 클릭 한번으로 쉽게 복사할 수 있도록 구성되어 있다. 또한 내가 크롤링한 이미지는 링크값만 가지고 있어서 사실 나도 링크값만 봐서는 이게 어떤 이미지인지 알수가 없었다. 그래서 시각적으로도 보여지는 탭을 하나만들자!해서 뚝딱 만들었다.
(좀 대충 만들기는 한거 같다. 디자인좀 바꿀까?)
크롤링해서 모아둔 이미지를 보니까 문제점이 너무 많았다. 첫째, 이미지 분류가 안 돼서 엉망이고, 둘째, 제대로 렌더링도 안 되는 이미지가 많았고. 마지막으로 가져온 이미지 중에 움짤이 아니라 유튜브 썸네일 같은 것도 섞여 있고, 대사 없는 이미지라 재미를 줄 수 없는 것도 잔뜩 있어서 내가 원하는 이미지가 아닌 경우가 너무 많았다. 결국 이걸 해결하려면 사람이 직접 검수를 해야 할 것 같았고, 그건 결국 내가 하나씩 다 확인해서 검수가 완료된 이미지들만 보여줘야 될 것 같았다.
(이미지가 지금 모인것만 천장이 넘는데 3장까지만 검수 하고서 포기하는거 아닌가 모르겠다.)
이미지 복사나 다운로드 기능을 제공하려고 하다 보니 CORS 문제가 생겨서, 지금의 인프라 구조로는 구현이 불가능하다는 걸 알게 됐다. 이걸 계기로 인프라 개선과 확장의 필요성을 절실히 느끼게 되었다. 월요일에 이런 고민을 하다 보니, 기존에 진행하던 무한도전 사진 박물관 프로젝트의 인프라 구조를 대대적으로 손봐야겠다는 결론에 이르게 됐다.
(아도겐으로 다 부숴버려야 할거 같다.)
결국 깃허브 페이지에서 호스팅하던 프론트 소스를 EC2의 nginx로 옮겼고 구성을 완료하였다. nginx를 쓰고자 했던 가장 큰 이유는 도메인의 적용후의 HTTPS 적용을 무료로 쉽게 적용 할 수 있는 방법이 존재하였기에 나에게 큰 매력으로 다가왔는데, 정말 적용하는데 5분도 채 걸리지 않았고 처음 해보는 나도 쉽게 완료하였다.
(https://letsencrypt.org/ko/)
뭐 이 이상의 깊은 이야기는 project 탭에서 진행해야겠다. 한번 말하기 시작하니까 또 deep하게 들어갈거 같다. 하여튼 프로젝트 구조를 변경중에 있으며 내부적으로 프론트 백엔드 소스를 뒤집어 엎고 있다. 단 하나의 목표. 재밌는 움짤의 복사/다운로드 기능을 위하여…!
(diary 탭에서는 여기까지...!)
아, 이번주 목요일에 큰 이슈가 예정되어 있었다. 그건바로 LG 유플러스 유레카 부트캠프 역량검사 당일날이었는데, 시험 후기 결론부터 말하자면…..
(으아아아아!!!! 망했다고!!!!!)
대외비라서 자세하게는 말 못하지만 결과는 보나보나 망했을거라고 예상중이다. 1기분들 후기에서는 시험 망해도 면접까지 오신 분들이 있기도하고 면접을 잘봐야 붙는거라고 꿀팁이 적혀있기는 했는데 나는 결과가 어떻게 나올지 모르겠다. 면접까지 간다면 이번에는 면접까지의 과정을 정말 준비를 빡세게 해야할거 같다는 생각만 든다.
(제발 한번만 더 기회를 주세요 절 증명할 수 있는 기회를.)
그나저나 면접이고 나발이고 코딩테스트 자신있어했던게 저번주 였던거 같은데, 실전은 다르다 라는걸 처음 알았고, 내가 놓친게 혼자 코테 문제풀이하는건 시간이 무제한으로 주어지고 내가 못푸는 문제는 조금 여유를 두고 천천히 고민하고 풀어도 풀기만하면 그만이지만, 기업코테는 주어진 시간내에 각 문제에 할당된 시간에 맞춰서 풀 수 있어야 한다는 점이다. 내가 너무 자만했다. 백준 골드 달았다고 좋아할 그개 아니라는걸 뼈저리게 느꼈다.
(오늘따라 재석이 형이 젊은 날의 청춘을 어떤 마음가짐으로 보냈을지 문득 궁금해진다.)
목요일이 지나고 난뒤에 금요일 하루는 그냥 푹 쉬었다. AWS 강의 마지막 하나 남은거 글이나 쓰면서 시간을 보내고 31일 역량검사 결과가 나오니 그전까지는 좀 마음을 비우고 기다리고자 한다. 주말에는 웹툰 정주행도 하고 운동도 하고 여러모로 몸관리도 하고 쉬면서 하고싶은걸 할 생각이다. 하고싶은거 중에 이번주 기록을 남기는 일도 포함이니 이 글을 쓰면서 시간을 보내기도 하고 있다.
12월 31일 이번년도의 마지막을 기념하며 글을 쓸지는 모르겠는데 이번년도를 돌아보면, 8월 말부터 시작된 공부가 굉장히 오래전 일 같지만 4-5개월 정도밖에 되지 않은게 정말 신기하다. github 커밋찍은걸 보니까 정확히는 8월 23일부터 시작한걸 알 수 있다. 뭔가 오래된거 같은데 얼마 안되었네 ?
(커밋 근데 왤케 많이찍었지 블로그 때문인가?)
그 쩗은 시간 동안의 성과를 나열해보자면, 깃허브 블로그 생성 및 운영, 무한도전 사진 박물관 프로젝트 시작, 다양한 인프런 강의 듣기, 백준 골드 티어 달성, 리눅스 마스터 2급 시험 완료, 정도 인거 같다. 좀 아쉬운건 인프런 강의듣고 나서 어딘가에 정리해두거나 기록을 해뒀으면 참 좋았을텐데 다시 듣고 정리해야 할거 같은게 좀 흠이다.
(인프런 강의를 거의 2개월 넘게 듣기만 했는데, 듣기만 하지말고 좀 기록해둘걸 그게 제일 아쉽다.)
내년에는 좀 더 성장한 내 모습을 기대해도 되겠지? 올해 4-5개월 동안 이 정도 성과를 냈으니, 내년은 더 기대가 되긴 한다. 더 건강하고 행복해져서 좋은 회사에 취업하는 나를 내년의 청사진으로 그려보고 싶다.
시간이 지나면 지날수록 그냥 흘려보내는 시간이 점점 더 아까워지는 것 같다. 정말, 재석이 형 마음이 이런 걸까 싶기도 하고. 20대 초반엔 그냥 가만히 있는 게 좋았는데, 요즘은 조금이라도 시간을 더 효율적으로 쓰고 싶다는 생각이 자꾸 든다. 지금 이 순간이 중요하다는 걸 이제야 깨달은 것 같은데, 너무 늦게 알게 된 건 아닐까 하는 생각도 든다.
뭔가 점점 쓸데없이 진지해지는 것 같으니, 이쯤에서 글을 마무리해야겠다. 이 글을 읽는 모든 사람들, 한 해 잘 마무리하고 내년에는 더 건강하고 행복한 한 해가 되길 바란다. 원래 국룰대로 오늘은 여기까지 짤을 써야 하지만, 이번만큼은 다른 짤로 대신해야겠다!
(다들 건강하고 행복하자. 일단 나부터!)
-
[docker] 기본 개념 및 설치 방법 (feat. m1 mac에 homebrew로 설치하기)
intro : docker의 기본 개념 및 m1 mac에 homebrew로 설치 방법에 대해서 알아보자.
Docker를 배우는 이유
Docker의 가장 큰 장점은 이식성이다. 이식성이란 특정 프로그램을 다른 환경으로 쉽게 옮겨 설치하고 실행할 수 있는 특성을 의미한다. 이를 통해 복잡하고 번거로운 설치 과정을 반복하지 않아도 되고, 필요한 환경을 즉시 실행할 수 있다는 장점이 있다. 또한, Docker는 운영 체제, 버전, 환경 설정, 옵션 등에 상관없이 항상 동일한 환경을 제공하여, 환경 차이로 인한 오류를 방지할 수 있다. 더불어, Docker의 컨테이너는 각 프로그램이 독립된 환경에서 실행되도록 보장하기 때문에, 서로 다른 프로그램 간의 충돌을 방지할 수 있다. 이러한 특성은 개발, 테스트, 배포 과정에서 일관성과 효율성을 높여 주며, Docker가 많은 개발자들에게 필수 도구로 자리 잡게 된 이유 중 하나이다.
Docker란?
컨테이너를 사용하여 각각의 프로그램을 분리된 환경에서 실행 및 관리할 수 있는 툴이다.
컨테이너(Container)란?
하나의 컴퓨터 환경 내에서 독립적인 컴퓨터 환경을 구성해서, 각 환경에 프로그램을 별도로 설치할 수 있게 만든 개념이다. 하나의 컴퓨터 환경 내에서 여러개의 미니 컴퓨터 환경을 구성할 수 있는 형태이다. 여기서 얘기하는 미니 컴퓨터를 보고 Docker에서는 컨테이너(Container)라고 부른다.
컨테이너(Container)의 독립성
컨테이너는 독립적인 컴퓨터 환경 이라고 말할 수 있다. 그렇다면 구체적으로 어떤 부분들이 독립적으로 관리되는지 알아두는 것이 중요하다. 우선, 저장 공간이 독립적이다. 각 컨테이너는 자신만의 저장 공간을 가지며, 일반적으로 A 컨테이너 내부에서 B 컨테이너 내부의 파일에 접근할 수 없다. 또한, 네트워크 역시 독립적입니다. 각 컨테이너는 고유한 네트워크를 가지며, 자신만의 IP 주소를 사용한다. 이러한 특성 덕분에 컨테이너 간의 충돌 없이 독립적인 환경에서 안전하게 애플리케이션을 실행할 수 있다.
(컨테이너의 독립성의 이해를 돕는 이미지)
이미지란?
Docker에서 이미지(Image)는 닌텐도의 칩처럼 특정 프로그램을 실행하기 위한 모든 요소를 포함하는 개념이다. 이미지는 프로그램 실행에 필요한 설치 과정, 설정, 버전 정보 등을 모두 담고 있어, 이를 기반으로 컨테이너(Container)를 실행할 수 있습니다. 예를 들어, Node.js 기반의 Express.js 서버 프로젝트를 이미지로 만들면, 이 이미지를 실행하는 순간 컨테이너 환경에서 서버가 실행된다. 복잡한 설치 과정을 거치지 않고도 손쉽게 프로그램을 실행할 수 있다. 마찬가지로 MySQL 서버 이미지도 실행과 동시에 MySQL 데이터베이스 환경을 제공하므로, 설치 없이 즉시 사용이 가능하다. Docker 이미지는 프로그램 실행에 필요한 모든 것을 포함하여 손쉬운 배포와 실행 환경을 제공한다.
Homebrew로 Docker 설치하기
Step1. 터미널에 brew 명령어로 docker 설치하기
본인의 터미널에서 다음과 같은 명령어를 실행한다.
brew install --cask docker
그렇다면 다음과 같이 설치가 진행된다.
(본인은 기존에 설치되어있다가 다시 재 설치하는거라 화면이 조금 다를 수 있다.)
Step2. 설치된 docker 버전 확인하기
docker가 정상적으로 설치되었는지 확인하기 위해서, 본인의 터미널에서 다음과 같은 명령어를 실행한다.
docker --version
docker compose version
(다음과 같이 화면에 버전이 출력된다면, 정상적으로 설치가 완료된 것이다.)
Step3. Launchpad에서 Docker 실행하기
brew로 설치할때 --cask 옵션은 GUI 프로그램을 설치할때 사용하는 명령어 옵션 인데, Launchpad로 들어가보면 다음과 같이 Docker가 보일것이다. 설치된 Docker를 클릭하여 실행해보자.
(클릭하면 실행된다.)
Step4. docker desktop 실행하기
그럼 화면상단에 docker 모양의 로고가 생기는데 우측 마우스로 클릭하고, Go to The DashBoard 클릭허자.
(Go to The DashBoard 클릭하면 docker desktop이 열린다.)
docker desktop이 다음 이미지 처럼 열리게 된다.
(docker desktop이 열린 모습이다.)
docker를 설치하는 방법에는 다양한 방법이 존재하지만, 차후 쿠버네티스 설정을 쉽게 하기위해 docker desktop을 설치하는 방식으로 설치해 보았다. docker desktop에서 설정값 하나만 켜주면 쿠버네티스를 사용할 수 있다. (쿠버네티스 글에서 다룰 예정)
[번외] Docker를 사용하여 Nginx 설치 및 실행하기
위 방법으로 설치한 docker를 통해 nginx를 설치하고 실행해보자.
Step1. nginx image 다운받기
docker를 통해 컨테이너에 nginx를 실행하려면 가장 먼저 nginx 이미지를 다운받아야 한다. 다음과 같은 명령어를 통해 이미지를 다운받자.
docker pull nginx
(nginx image가 다운이 완료된 모습이다.)
Step2. nginx image 확인하기
docker를 통해 다운받은 image 목록을 조회할수 있는데 다음과 같은 명령어로 조회할 수 있다.
docker image ls
(docker를 통해 다운받은 image 목록이 조회되는 모습이다.)
Step3. docker 명령어를 통해 nginx 실행하기
다운받은 nginx 이미지를 다음과 같은 명령어를 통해 실행할 수 있다. 이 명령어는 이름이 webserver인 새로운 컨테이너를 생성하고, NGINX 이미지를 기반으로 백그라운드에서 실행하며, 컨테이너의 80번 포트를 호스트의 80번 포트와 연결하여 로컬 환경에서 http://localhost 를 통해 NGINX 웹 서버를 사용할 수 있도록 설정하는 뜻을 축약한 명령어 이다.
docker run --name webserver -d -p 80:80 nginx
(위 명령어를 실해한 후 터미널 모습이다.)
Step4. nginx 서버 잘 실행되는지 확인하기
http://localhost로 접근하면 다음과 같은 이미지처럼 화면이 나온다. 위 명령어에서 80번 포트로 접근시에 컨테이너의 80번 포트로 접근히기로 매핑이 되어있기 떄문에 정상적으로 접근이 된 모습이다.
(포트는 80번이기때문에 localhost로 접근만 하면 nginx 페이지가 나온다.)
Step5. 실행되고 있는 컨테이너 목록 확인하기
아래 명령어를 실행하면 기존에 nginx 를 실행하였던 컨테이너를 조회할 수 있다. 현재 실행중인 것을 알 수 있다.
docker ps
(nginx를 실행시킨 컨테이너 이름이 webserver 이다.)
Step6. 실행중인 nginx 컨테이너 정지하기
다음과 같은 명령어로 nginx 컨테이너를 정지할 수 있다. 기존에 실행하였던 nginx 컨테이너 이름을 webserver로 지정하였는데 해당 이름으로도 정지를 할 수 있다.
docker stop webserver
(webserver 컨테이너 정지)
Step7. 정지한 nginx 컨테이너 삭제하기
아래 명령어를 통해 위 단계에서 정지한 nginx 컨테이너를 삭제할 수 있다.
docker rm webserver
(webserver 컨테이너 삭제)
다음으로
이번 시간에는 Docker의 기본 개념을 학습하고, M1 Mac에서 Homebrew를 사용해 Docker Desktop을 설치한 뒤, NGINX를 실행해 보았다. 다음 시간에는 현업에서 자주 사용하는 CLI 명령어들을 학습해 볼 예정이다.
-
[aws] s3와 cloudfront를 활용한 웹 페이지 배포
intro : s3와 cloudfront를 활용한 웹 페이지 배포하는 방법에 대해서 알아보자.
S3는 파일 저장 서비스 아닌가?
이전글에서 S3는 파일을 저장하는 서비스라고 정의하였었다. 사실 S3는 의외의 부가적인 기능을 보유하고 있는데 바로 정적 웹 사이트 호스팅 기능이다. 쉽게 표현하자면 웹 서비스를 다른 사용자들도 쓸 수 있게 인터넷에 배포하는 것을 말한다.
CloudFront란?
컨텐츠(파일, 동영상 등)를 빠르게 전송하게 해주는 서비스이다. AWS의 Cloud Front 서비스가 어떻게 컨텐츠를 빠르게 전송해 주는지 아래 이미지를 통해 작동 과정을 눈으로 확인해보자.
(Cloud Front 작동 과정)
이미지와 같은 정적 파일을 로드할 때, CDN(Content Delivery Network)를 사용하기 전에는 원본 서버(Origin Server)로부터 물리적으로 거리가 먼 사용자는 파일을 로드하는 데 상당한 시간이 걸릴 수 있다. 이는 네트워크 지연(latency) 때문이다. AWS EC2 인스턴스 생성 시 리전을 한국(서울)으로 설정하는 이유도 같은 맥락이다. 서버와 사용자 간의 물리적 거리가 가까울수록 네트워크 지연이 줄어들고, 응답 속도가 빨라져 사용자 경험이 향상된다. 따라서, CDN은 전 세계 여러 지역에 캐시 서버를 두어 사용자와 가까운 서버에서 콘텐츠를 제공함으로써 이런 문제를 효과적으로 해결할 수 있는 중요한 역할을 하는데 그게바로 AWS의 Cloud Front 이다.
S3 + CloudFront 조합을 사용하는 이유
S3만으로도 웹페이지를 배포하는 것은 충분히 가능하다. 그렇다면 왜 S3 + CloudFront 조합을 사용하는 걸까? 그 이유는 CloudFront가 콘텐츠 전송 성능을 크게 향상시키기 때문이다. 전 세계 여러 엣지 로케이션(Edge Location)을 통해 사용자와 가까운 서버에서 콘텐츠를 제공하여 지연(latency)을 줄이고, 응답 속도를 개선할 수 있다. 또한, HTTPS를 적용하려면 CloudFront를 사용하는 것이 더 간편하고 효과적다. S3 자체로도 HTTPS를 구성할 수 있지만, 추가적인 인증서 설정과 도메인 연결이 필요해 복잡도가 높다. CloudFront를 사용하면 이러한 작업이 간소화되며, 보안을 강화할 수 있는 장점이 있다.
S3, CloudFront를 활용한 아키텍처 구성
(사용자의 S3+Clout Front 조합의 웹페이지 접근 과정)
S3 + CloudFront 조합으로 웹 페이지 배포하기
이제 본격적으로 S3와 CloudFront를 통해 웹 페이지를 배포해 보도록 하겠다.
Step1. S3 버킷 생성하기
가장 먼저 해야 할일은 S3 버킷을 생성해야하는데 다음과 같이 AWS 콘솔에서 S3를 검색하여 접속하자.
(버킷 만들기 버튼을 눌러주자.)
버킨 만들기 버튼을 누르면 다음과 같은 화면이 보이게 되고, 버킷이름을 먼저 작성해주자 나같은 경우는 api-server-web-page 라고 지정하였다.
리전은 항상 서울로 설정하는건 잊으면 안된다.
(버킷 이름을 잘 지정해 주자.)
그 이후에 이 버킷의 퍼블릭 액세스 차단 설정 항목에서 모든 퍼블릭 액세스 차단을 언체크 해야한다. 체크박스를 풀어주어야 다른 익명의 사용자들이 S3에 저장된 나의 웹 페이지를 접속할 수 있다. 또한 모든 퍼블릭 액세스 차단을 비활성화하면 이 버킷과 그 안에 포함된 객체가 퍼블릭 상태가 될 수 있습니다. 항목에서의 현재 설정으로 인해 이 버킷과 그 안에 포함된 객체가 퍼블릭 상태가 될 수 있음을 알고 있습니다.는 체크해 주어야 한다. 아래 이미지를 잘 확인하자.
(체크 언체크 항목 잘 확인하자.)
위 과정을 완료한 후에 다른 부분은 변경하지 않고 화면 우측 하단의 버킷 만들기 버튼을 클릭한다.
(버킷 만들기 버튼 클릭하자.)
위 과정을 정상적으로 진행하였다면 다음과 같이 버킷이 생성 완료된것을 확인 할 수 있다.
(버킷 생성 성공!)
버킷을 생성하였지만, 위 과정을 통해 생성된 버킷은 아직 권한이 열리는건 아니다. 따로 버킷의 권한을 수정하여 정책을 생성해 주어야 한다. 다음과 같이 생성된 버킷의 권한 탭으로 이동하자.
(생성된 버킷을 클릭하고 권한탭으로 이동하면 나오는 화면이다.)
하단으로 조금 내리면 버킷 정책 영역이 보이는데 이부분의 편집 버튼을 클릭하자.
(버킷 정책의 편집 버튼 클릭)
화면 중앙의 새문 추가 버튼을 클릭하자. 본격적으로 S3의 접근 권한을 설정할 것이다.
(새 문 추가)
작업 필터링 영역에 S3 입력 후 체크, GetObject 입력 후 체크하면 다음과 같은 화면이 된다.
(S3 > GetObject 체크)
그 뒤에 리소스 추가를 누르고 다음과 같이 리소스 유형으로 object 리소스 ARN는 기존에 arn:aws:s3:::{BucketName}/{ObjectName} 라고 작성되어 있을텐데 {BucketName}에는 본인의 버킷 이름 즉 나같은 경우는 api-server-web-page를 입력하고, {ObjectName}는 *로 모든 파일에 대해서 접근을 허용해준다. 그 뒤로 리소스 추가 버튼으로 작업을 완료해주자.
(리소스 추가)
정책의 Principal 부분을 보면 기존에는 {} 라고 입력이 되어 있을텐데 "*" 으로 입력해준다. 이후 변경 사항 저장을 눌러서 버킷 정책 편집을 완료해 주자.
("Principal": "*" 입력)
다음과 같이 버킷 정책 편집이 잘 완료된 것을 확인 할 수 있다.
(버킷 정책 편집 완료)
Step2. S3에 파일 업로드 및 웹 호스팅 설정하기
일단 가장 먼저, 생성된 버킷을 아래 이미지 페이지로 이동하여 확인하자.
(범용 버킷에 가면 내가 생성한 버킷이 확인된다.)
내가 생성한 버킷의 이름이 파란색으로 되어있는데 해당 이름을 클릭하면 다음과 같은 페이지로 이동할수 있고 파일을 업로드할 수 있는 업로드 버튼이 보인다.
(업로드 버튼을 누르자.)
그러면 다음과 같은 화면이 나오고 여기서 파일 추가 버튼을 눌러서 간단한 index.html 파일을 업로드 해보자 (업로드 버튼까지 클릭하자.)
(index.html 파일 소스는 다음과 같이 작성되어있다.)
(내가 알만한 문구로만 작성하면 된다.)
그러면 이렇게 버킷에 index.html 파일이 업로드 된 것을 확인할 수 있다.
(버킷에 파일 정상 업로드 확인 완료)
S3에는 파일 업로드 다운로드 기능 뿐만 아니라, 정적 웹사이트 호스팅 기능도 존재한다고 했는데 우리는 지금 생성한 S3의 index.html 파일을 정적 웹사이트 호스팅 기능을 이용해 볼 것이다. 가장먼저 다음과 같이 버킷의 속성 탭으로 이동해보자.
(버킷의 속성 탭으로 이동해보자.)
아래로 화면을 쭉 내리면 정적 웹 사이트 호스팅 영역이 나오는데 편집 버튼을 눌러주자.
(편집 버튼을 클릭하자.)
지금은 간단한 정적 웹페이지를 호스팅 하는 것이기 때문에 정적 웹 사이트 호스팅 영역은 활성화 버튼 클릭, 인덱스 문서는 index.html을 입력하고 아래로 내려서 변경사항 저장 버튼을 눌러준다.
(변경 사항 저장 버튼 클릭 잊지말자.)
변경사항 저장 버튼을 클릭하고 나서, 새롭개 갱신된 화면에서의 제일 하단을 보면 다음과 같이 정적 웹 사이트 호스팅 영역이 존재한다. 버킷 웹 사이트 엔드포인트 의 주소값이 현재 정적 웹사이트 호스팅이 된 주소값인데 클릭해보면 index.html 파일로 접근하게 된다.
(버킷 웹 사이트 엔드포인트의 주소값을 브라우저에 입력해보자.)
정상적으로 접근되는것을 확인할 수 있다.
(S3의 정적 웹사이트 호스팅은 성공이다.)
Step3. CloudFront 생성하기
S3의 정적 웹사이트 호스팅을 완료하였으니 이제는 CloudFront를 생성해볼 차례이다. 가장먼저 AWS 콘솔창에서 CloudFront를 검색해서 접속하자. 다음과 같은 화면이 보일것이다.
(CloudFront 페이지 접속)
CloudFront 배포 생성 버튼을 클릭하자. 그럼 다음과 같은 화면을 볼 수 있다.
(이제부터 각 영역에 맞는값을 입력해 볼 것이다.)
가장 먼저 Origin domain을 설정할 것인데, CloudFront가 원본 파일의 주소가 어디있는지를 물어보는것이다. 우리는 원본파일을 S3에 만들어 두었기에 S3의 주소를 클릭하면 된다.
(S3의 주소를 클릭한다.)
그 뒤에 Origin domain의 아래의 경고 문구가 보이는데 웹 사이트 엔드포인트 사용 버튼클릭을 해준다. AWS 내부 구조상 웹사이트 엔드포인트 사용하는것이 더 좋다고 권장하고 있다. 그러면 http://api-server-web-page.s3-website.ap-northeast-2.amazonaws.com/ 이렇게 이전에 S3를 정적 웹사이트 호스팅 하고나서 버킷 웹 사이트 엔드포인트의 주소값을 확인할 수 있었는데 그 주소로 Origin Domain이 설정이 된다.
(S3의 주소를 클릭한다.)
아래로 내리면 기본 캐시 동작의 영역이 보이는데 이 부분에서 뷰어 프로토콜 정책의 Redirect HTTP to HTTPS를 선택한다. HTTP로 접근하는 요청을 HTTPS로 리디렉션을 하겠다는 설정이다.
(HTTPS 리디렉션 설정)
기본 캐시 동작 영역 이후에 또 아래로 화면을 더 내리면 웹 애플리케이션 방화벽(WAF) 영역이 보이는데 보안 보호 비활성화을 선택해준다. 이걸 선택한다고 해서 보안이 엄청 약해지거나 하는건 아니다. 기본적으로 비활성화를 하더라도 보안이 충분하기 때문에 만약 디테일한 보안을 추가하고싶은 경우 보안 보호 활성화를 눌러주면 된다.
(보안 보호 비활성화 선택)
이후 화면을 더 내리면 설정 영역이 보이는데 CloudFront를 어느 지역에 활성화 시킬것인지 선택하는 부분이다. 우리는 아시아권 중심으로만 서비스를 제공한다 가정하고 북미, 유럽, 아시아, 중동 및 아프리카에서 사용 으로 선택한다.
(아시아권 중심으로 한다는 가정하에 선택하는 것이다.)
같은 설정 영역에 기본값 루트 객체 - 선택 사항이 존재하는데 해당 부분은 / 경로로 접근할때 어던 페이지를 보여줄 것인지 작성하는 부분이다. 당연히 우리는 index.html 파일을 보여줘야 하기떄문에 index.html을 입력한다.
(index.html 입력)
이후 아래의 배포 생성 버튼을 클릭하여 마무리한다.
(배포 생성 버튼 클릭)
배포 생성 버튼을 클릭하고나서 CloudFront > 배포페이지로 이동하면 위 단계를 거쳐서 생성된 CloudFront가 보인다. 이때 세부 정보에 배포 도메인 이름이 보이는데 해당 도메인이 CloudFront로 접근할수 있는 주소이다.
(도메인 주소를 브라우저에 입력해보자.)
어느 정도 시간이 지나고 나면 브라우저에 URL을 입력했을 때, CloudFront를 통해 S3에 호스팅된 정적 웹사이트에 정상적으로 접근할 수 있음을 확인할 수 있다.
(정상 배포 성공!)
Step4. 도메인 연결 및 HTTPS 적용하기
지금까지의 단계를 정리하자면 S3에 index.html을 업로드하고 정적 웹 사이트 호스팅을 한뒤에, CloudFront를 통해서 S3에 접근할수 있도록 설정하였다. 다만 아직 HTTPS가 적용되지 않아서 HTTPS를 적용할 것인데, 가장먼저 ACM 발급 작업이 선행되어야 한다.
(ACM 페이지로 이동하자.)
주의사항
CloudFront에 ACM을 적용하기 위해서는 발급을 리전이 한국이 아니라, 버지니아 북부로 설정해야 한다. 굉장히 중요하니 꼭 확인하자.
ACM 페이지로 이동하였다면 요청 버튼을 클릭하여 다음과 같은 화면에서 별 다른 수정 없이 또 다음 버튼을 클릭해 준다.
(다음 버튼 클릭)
도메인 이름을 입력한다. 기존에 구매했었던 도메인 주소를 입력하였다.
(오랜만이야 visiblego.com)
도메인 이름을 입력한뒤에 다른 부분은 수정하지 않고 화면을 내리면 나오는 요청 버튼을 클릭한다.
(요청 버튼 클릭)
그럼 이미지처럼 화면이 보이는데 중앙에 Route 53에서 레코드 생성 버튼이 보인다 도메인에 CNAME을 추가하는 것인데 해당 버튼을 클릭해 준다.
(Route 53에서 레코드 생성 버튼 클릭)
(레코드 생성 버튼 클릭)
CNAME이 추가된것을 확인 할 수 있다.
(CNAME을 도메인 레코드에 정상적으로 추가된 것을 알 수 있다.)
여기까지 완료되면 이 인증서를 CloudFront로 이동하여 적용해야 한다. 다음 이미지와 같이 이전 단계에서 생성한 CloudFront의 설정 항목으로 이동하여 편집 버튼을 클릭한다. 그럼 하기 페이지를 확인할 수 있다.
(CloudFront 설정 페이지)
화면을 내리면 대체 도메인 이름(CNAME) - 선택 사항이 보이는데 여기서 항목 추가버튼을 클릭하여 ACM 인증서 발급을 받은 도메인 이름을 작성한다. 나같은 경우는 visiblego.com 을 작성하였다. 또한 사용자 정의 SSL 인증서 - 선택 사항에 ACM에서 발급받은 인증서를 선택해 주면 끝이다. 화면 쭉 내려서 변경 사항 저장 버튼을 클릭해서 설정 편집을 완료하자.
(이미지 보고 잘 입력하자.)
그 뒤에 마지막으로 CloudFront에 Route53의 도메인을 연결해야 한다. Route 53의 호스팅 영역 페이지로 이동하자. 이제 레코드 생성을 해야한다.
(레코드 생성 버튼을 누르자.)
그 뒤에 별칭을 활성화 하고, 엔드포인트를 CloudFront 배포에 대한 별칭으로 설정한다. 그러면 리전 항목이 배포 선택으로 변경되는데 CloudFront의 주소값을 설정할 수 있다. 값을 잘 선택한 뒤에 레코드 생성 버튼을 클릭해 주자.
(이미지를 잘 보고 설정하자.)
정상적으로 레코드가 생성되었다고 알림창으로도 알려준다.
(레코드 생성 완료!)
어느정도 시간이 지난 뒤에 CloudFront에 연결한 도메인(visiblego.com)으로 접속해보면 정상적으로 HTTPS로 접근되는것을 확인할 수 있다.
(S3 + CloudFront + HTTPS 완료!)
S3 + CloudFront 종료하기
먼저 CloudFront를 종료해보도록 하겠다. CloudFront 배포 페이지로 이동해서 비활성화 버튼을 클릭하자.
(비활성화 버튼 클릭)
그럼 다음 이미지 처럼 사용 중지가 된다. 비활성화가 되어야 삭제를 할 수 있기 때문에 시간을 기다려 주어야 한다.
(사용중지 상태이다.)
시간이 어차피 걸리는 동안 S3 버킷과 객체를 삭제하자. 가장먼저 버킷을 삭제하기 전에 버킷안의 객체를 전부 먼저 삭제해야한다. index.html 파일을 삭제하자. index.html 파일을 선택하고 삭제 버튼을 클릭하자.
(객체를 전부 삭제해준다.)
버킷을 삭제하자.
(선택하고 삭제버튼을 클릭하자.)
버킷과 객체를 삭제하는동안 CloudFront가 비활성화가 되기를 바라면서 CloudFront 페이지로 이동한다. CloudFront 배포 ID를 선택하고 삭제 버튼을 클릭하여 마무리 하자.
(CloudFront 배포 ID를 선택하고 삭제 버튼을 누르자)
Route53의 호스팅 영역에서의 도메인 레코드 값 CNAME, A타입 레코드 값을 삭제하는건 이미지를 생략한다.
다음으로
다음 글은 기존에 작성했던 AWS 내용을 프로젝트에 적용하는 과정에서 발생한 문제점이나 트러블슈팅에 관한 내용이 될 것 같다. 예를 들어, EC2 인스턴스에 swap을 설정하는 방법이나, RDS 설정 시 불필요한 비용이 발생하지 않도록 항목을 수정하는 방법과 같은 실용적인 주제를 다룰 예정이다. 다음글이 작성된다면, 해당 글의 내용을 수정하고 다음글의 대한 간략한 설명과 링크를 작성하도록 하겠다.
-
[infinite-challenge] 무한도전 사진 박물관 프로젝트 1
intro: 나의 첫 번째 프로젝트
2024년도가 2주일도 채 남지 않은 시점인 2024년 12월 19일부터 시작된 무한도전 사진 박물관 프로젝트에 대한 기록을 남겨보고자 한다. 개발 블로그 취지에 맞는 글을 처음 써보는 것이라 어떻게 시작하고 마무리해야 할지 고민이 많고 부족한 점이 있겠지만, 내가 어떤 과정을 통해 무엇을 만들고자 했는지, 적용한 기술과 트러블슈팅, 그리고 개발 과정 속의 고민들을 녹여내고자 한다.
프로젝트 시작 계기 및 주제와 방향성
사실 프로젝트를 시작하게 된 계기는 정말 간단하다. HTML/CSS 강의를 듣던 중 Flex 레이아웃과 Grid 레이아웃에 대한 연습을 하고 싶었다. React 같은 프레임워크를 사용하는 대신, index.html 파일을 만들고 style.css 파일로 간단히 레이아웃을 잡아보며 GitHub를 이용해 정적 페이지 호스팅을 해보자는 아이디어가 이번 프로젝트의 초안이 되었다.
학부생 시절, 캡스톤 디자인이나 팀 프로젝트를 진행할 때 가장 어려웠던 과정은 바로 ‘무엇’을 만들 것인가?였다. 개발을 통해 어떤 서비스를 제공할지 설계하고 기획하는 것이 굉장히 힘들었다. 그래서 이번 프로젝트는 단순히 페이지를 만들어보는 것이 목표였기 때문에 내가 좋아하는 것을 주제로 하기로 했다.
그렇다면 내가 좋아하는 게 무엇일까? 곰곰이 생각해보니, 나는 무한도전 짤을 자주 찾아보고 사용하며, 레전드 영상을 지금도 자주 시청한다. 무한도전이라는 프로그램 자체를 정말 좋아했고, 지금도 밥을 먹을 때나 기분이 좋을 때, 혹은 안 좋을 때에도 쿠팡플레이로 재방송을 챙겨볼 정도로 좋아한다. 이런 애정을 바탕으로 무한도전을 중심으로 한 서비스를 만들어 보면 어떨까 하는 생각이 들었다.
그리하여 무한도전에서 나온 사진들 중, 보기만 해도 웃음이 나는 사진들을 사용자들에게 제공해 보자!라는 아이디어를 가지고 나의 첫 번째 프로젝트는 시작되었다.
초기 페이지 구성
페이지의 레이아웃은 큰 틀은 grid 레이아웃으로 잡았다. 아무래도 내부적으로는 flex 레이아웃을 이용해서 배치를 어떻게할지 고민하는게 더 낫다고 생각했고 두가지 레이아웃을 혼합해서 사용해보고 공부한 내용을 적용해 보고 싶었기에 깊은 고민은 하지않고 바로 실행으로 옮겼다. 이 글을 쓰는 시점이 이미 어느정도 개발이 진행되고 있는 과정속에서 늦은 기록이 진행되고 있기에 완전 초창기의 사진은 없으나 다행히 초창기에서 조금더? 지난 시점의 사진은 남겨 둘 수 있을 것 같다.
(페이자 상단)
(페이지 하단)
위 페이지에 대해서 간단히 소개를 하자면, 페이지 하단의 버튼 당겨요! 버튼을 누르면 프로젝트 폴더안에 저장해두었던 움짤 사진들이 랜덤으로 화면상에서 돌아가면서 그중에 하나가 화면상에 보여지게 되고 페이지에 폭죽이 터진다. 초기의 목표가 사용자에게 무한도전의 웃긴 사진을 보여주어 웃음을 주는게 목표중에 하나였기에 js까지 동원하여 기능을 구현하였고 레이아웃 연습도 했고 기능적으로도 나쁘지 않다고 생각했기에 이대로 마무리 하고자 하였었다.
페이지 확장
위 이미지를 보면 탭이 두가지가 있다. 기존에 설명하였던 홈 화면과, 방명록 탭이 존재하는데 페이지를 다만들고 주변 친구들 혹은 가족들에게 보여주고 반응이 어떨지 궁금했고 개인적인 생각 혹은 피드백을 받으면 좋을거 같다는 생각이 들었다. 그러나 정적 페이지 이기에 페이지가 새롭게 빌드돠거나 새로고침만 해도 데이터가 날라가기에, 영구적으로 데이터를 저장할 공간이 필요하였다. 하지만 그렇다고 해서 AWS의 RDS를 쓰기에는 비용적으로도 문제가 있다고 생각했고 애초에 이 시점에서는 ‘많은 사용자가 이용하는 서비스를 만들자’ 라는 생각을 하지도 않았었기 때문에 무료로 사용가능하면서 클라우드 서비스로 제공되는게 있으면서 간단하게 사용할수도 있고 차후에 다른 DB로 변경하게 되더라도 문제가 없을만한 서비스를 찾아보게 되었고 결국에는 FireBase의 Realtime Database를 선택하게 되었다.
FireBase의 Realtime Database가 뭔데?
Firebase의 Realtime Database는 Firebase에서 제공하는 클라우드 기반의 NoSQL 데이터베이스로, 실시간 데이터 동기화를 지원하는 서비스이다. 별도의 EC2 환경에 설치할 필요 없이 클라우드 서비스를 통해 손쉽게 이용할 수 있어 매우 편리하다. 설치 과정이 필요 없다는 점이 큰 장점이며, 데이터베이스 생성과 활용도 간단하여 간단한 프로젝트에서 NoSQL을 도입하려는 경우 Firebase를 적극 추천한다. 실시간으로 데이터가 동기화된다는 장점이 있다고 하는데, 가끔 데이터 업데이트가 될때 FireBase 콘솔에서 업데이트 된 항목이 주황색 배경으로 빛나서, 변경된 항목이 무엇인지 알 수 있는것 같기는 하다.
(FireBase 홈페이지)
방명록 페이지 개발
위 DB가 연동이 되고 나니, 방명록 기능 개발은 일사천리였다. 프론트 방명록 페이지에서 입력받은 값을 FireBase에 저장하고 해당 값을 화면상에 보여주기만 하면되는거니까 매우 쉽게 다음 이미지와 같이 방명록 페이지 개발이 완료되었다.
(방명록 페이지 개발)
서버 개발의 시초
이 시점에서 사실 레이아웃 연습도 해볼만큼 해봤고, 방명록 페이지까지 만들어서 간단하게 주변 사람들 반응도 볼수 있었고 재미도 있었다. 근데 어쩌다보니 욕심이 나는게 더 다양한 이미지를 보여주면 어떨까? 사람들이 더 많은 재밌는 짤을 내 사이트에서 쉽게 찾는다면? 복사 및 다운로드를 쉽게 할 수 있다면 좋지 않을까? 라는 생각이 들었다. 그렇게 생각을 하다보니까 이미지를 크롤링 하는 서버가 필요하겠다! 싶었고, AWS의 EC2를 이용해야겠다는 생각이 들었다.
Selenium? Google Custom Search API?
그렇다면 크롤링을 하는 방법 부터 정해야 하는데 가장 유명한 방법은 파이썬에서도 자주 사용하는 Selenium 이었다. 근데 크롤링을 하려면 대상 페이지에 대한 분석이 필수이며 해당 DOM 트리의 구조를 잘 알아야 쉽게 크롤링을 할 수 있는데 구글을 상대로 페이지 분석을 해서 합법적인 방법으로 크롤링 할 수 있을까? 라는 생각이 들었다. 이전 학부생 시절 학습용으로 하던 구글 이미지 크롤링의 경험상 유지보수도 복잡하고 힘들었던 기억이 나기도하고, 네이버 이미지 크롤링을 하다가 사이트 차단을 일시적으로 Block을 먹어본적도 있었기에 뭔가 Selenium은 손이 가지 않는 방법으로 판단이 되었다. 그렇다면 좀 더 합법적이면서 확실하게 크롤링을 할 수 있는 방법이 없을까? 해서 찾아보니, 구글에서는 다음과 같은 API를 제공했다.
(Google Custom Search API 페이지)
이전에 Google Vision API를 사용해본 경험도 있고 Google의 Open API에 대한 인식이 굉장히 좋았기에 바로 적용해 보고자 하였다. 결과는? 당연히 굉장히 만족스러웠다. 리턴값도 굉장히 다양하게 반환되며 이전페이지와 다음페이지의 여부에 대한 정보도 반환되어 부가적으로 쓸모있는 정보들도 반환받을 수 있으며 당연히 내가 원하는 이미지의 주소값도 반환 받을 수 있었다.
// return 값 일부 중 발췌
{
"kind": "customsearch#search",
"url": {
"type": "application/json",
"template": "https://www.googleapis.com/customsearch/v1?q={searchTerms}&num={count?}&start={startIndex?}&lr={language?}&safe={safe?}&cx={cx?}&sort={sort?}&filter={filter?}&gl={gl?}&cr={cr?}&googlehost={googleHost?}&c2coff={disableCnTwTranslation?}&hq={hq?}&hl={hl?}&siteSearch={siteSearch?}&siteSearchFilter={siteSearchFilter?}&exactTerms={exactTerms?}&excludeTerms={excludeTerms?}&linkSite={linkSite?}&orTerms={orTerms?}&dateRestrict={dateRestrict?}&lowRange={lowRange?}&highRange={highRange?}&searchType={searchType}&fileType={fileType?}&rights={rights?}&imgSize={imgSize?}&imgType={imgType?}&imgColorType={imgColorType?}&imgDominantColor={imgDominantColor?}&alt=json"
},
"queries": {
"request": [
{
"title": "Google Custom Search - 무한도전",
"totalResults": "158000000",
"searchTerms": "무한도전",
"count": 10,
"startIndex": 1,
"inputEncoding": "utf8",
"outputEncoding": "utf8",
"safe": "off",
"cx": "",
"searchType": "image"
}
],
"nextPage": [
{
"title": "Google Custom Search - 무한도전",
"totalResults": "158000000",
"searchTerms": "무한도전",
"count": 10,
"startIndex": 11,
"inputEncoding": "utf8",
"outputEncoding": "utf8",
"safe": "off",
"cx": "",
"searchType": "image"
}
]
},
"context": {
"title": " infinite challenge-search-engine"
},
"searchInformation": {
"searchTime": 0.294487,
"formattedSearchTime": "0.29",
"totalResults": "158000000",
"formattedTotalResults": "158,000,000"
},
"items": [
{
"kind": "customsearch#result",
"title": "무한도전] (믿고 보는 몸개그 BEST) 유재석이 찐으로 웃으면 뭐다 ...",
"htmlTitle": "<b>무한도전</b>] (믿고 보는 몸개그 BEST) 유재석이 찐으로 웃으면 뭐다 ...",
"link": "https://i.ytimg.com/vi/CCTDvm8Va78/hq720.jpg?sqp=-oaymwEhCK4FEIIDSFryq4qpAxMIARUAAAAAGAElAADIQj0AgKJD&rs=AOn4CLAxeGqCiiAsXoEgd5yJ0CaQOwSFSw",
"displayLink": "m.youtube.com",
"snippet": "무한도전] (믿고 보는 몸개그 BEST) 유재석이 찐으로 웃으면 뭐다 ...",
"htmlSnippet": "<b>무한도전</b>] (믿고 보는 몸개그 BEST) 유재석이 찐으로 웃으면 뭐다 ...",
"mime": "image/jpeg",
"fileFormat": "image/jpeg",
"image": {
"contextLink": "https://m.youtube.com/watch?v=CCTDvm8Va78&t=77s",
"height": 386,
"width": 686,
"byteSize": 70317,
"thumbnailLink": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTtN7UlkqwnJ8oTC3Cs2NYNnrqETJB4tWO7OkKcP2-_tnyEkYDYczRnG8s&s",
"thumbnailHeight": 78,
"thumbnailWidth": 139
}
},
{
"kind": "customsearch#result",
"title": "무한도전 - 나무위키",
"htmlTitle": "<b>무한도전</b> - 나무위키",
"link": "http://i.namu.wiki/i/MDO4mGHS56yu2vBufeG8r05-cIY_aUfNya_edTqEKbjfhn43p4odUEq0y9zuKCOj5Jk4r67VmQACE2BSkWjxbg.svg",
"displayLink": "namu.wiki",
"snippet": "무한도전 - 나무위키",
"htmlSnippet": "<b>무한도전</b> - 나무위키",
"mime": "image/svg+xml",
"fileFormat": "image/svg+xml",
"image": {
"contextLink": "https://namu.wiki/w/%EB%AC%B4%ED%95%9C%EB%8F%84%EC%A0%84",
"height": 256,
"width": 350,
"byteSize": 7438,
"thumbnailLink": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSE-0T99jgFxHkru4hJWnv-JClGeLqfNZONbCxgNDK6M91tlJ-tobIH&s",
"thumbnailHeight": 88,
"thumbnailWidth": 120
}
},
{
"kind": "customsearch#result",
"title": "무한도전] ☆출구없는 매력☆ 고객 만족도 1위! 태국 전통쇼 계승자 ...",
"htmlTitle": "<b>무한도전</b>] ☆출구없는 매력☆ 고객 만족도 1위! 태국 전통쇼 계승자 ...",
"link": "https://i.ytimg.com/vi/9zcstq0pk74/hqdefault.jpg",
"displayLink": "www.youtube.com",
"snippet": "무한도전] ☆출구없는 매력☆ 고객 만족도 1위! 태국 전통쇼 계승자 ...",
"htmlSnippet": "<b>무한도전</b>] ☆출구없는 매력☆ 고객 만족도 1위! 태국 전통쇼 계승자 ...",
"mime": "image/jpeg",
"fileFormat": "image/jpeg",
"image": {
"contextLink": "https://www.youtube.com/watch?v=9zcstq0pk74",
"height": 360,
"width": 480,
"byteSize": 27547,
"thumbnailLink": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRdW19HUY4FUO5X_FU9S2FEpM6bh0h3n6wJs3eQ9HOBRu1oBpuSUBz6S18&s",
"thumbnailHeight": 97,
"thumbnailWidth": 129
}
}
}
위 리턴값을 보자마자 내가 손수 selenium으로 크롤링해도 저거보단 데이터 적게 추출할거 같아서 크롤링을 하는 방식은 해당 API를 통해 진행해야겠다고 결론을 내렸다.
Google Custom Search API의 단점
지금까지는 API의 장점만을 이야기 하였는데 사실 단점도 존재한다. 가장 큰 단점은 API 파라미터에 특정 인덱스에서 부터 검색을 할수 있는 값이 존재하는데(startIndex) 약간 페이지 번호 같은 느낌이다. 위 결과값을 보면 totalResults 값이 158,000,000 인데, 이상하게 1부터 200까지의 값만 허용해주고 그 이외의 값은 403코드를 반환한다. 나는 하나의 키워드로 많은 데이터를 크롤링 하기를 원했는데, 원하는 바는 해당 API를 통해서 이루지는 못하게 되었다. 또 다른 단점으로는 무료버전에서의 키워드 검색 허용 일일 제한량은 100회이다. 그렇기에 1회 검색시에 10개의 게시글이 조회되고 , 하루에 대략적으로 1000개 정도의 게시글을 API로 정보를 리턴받을 수 있다.
서버 개발의 시작
크롤링 서버를 만들고자 하였으니, 크롤링 하는 방식이 정해지니 나머지는 일사천리도 진행되었다. 스프링부트 프로젝트를 생성하고 API를 각 정시마다 호출할수 있도록 Component를 Bean으로 등록하고 크롤링한 정보를 위 방명록 페이지 개발시에 연동했었던 FireBase RealTime DataBase에 저장할 수 있도록 구성하였다.
이미지 저장? 이미지 원본 URL 저장?
크롤링은 API를 호출하면 return 받는 값을 통해 이미지 원본 URL 값을 알 수 있었고 해당 URL을 통해 이미지를 다운로드 받을 수 있었다. 그런데 이미지를 다운받기 시작하면서 생각하지 못했던 부분이 있는데 이미지를 어디에 저장하지?에 대한 질문을 스스로 개발중에 하지 않았었고 크롤링에만 매몰되어 이 시점에서 이미지를 계속해서 다운을받아서 로컬에 저장할지, 혹은 외부 스토리지에 저장을 할지, 혹은 이미지 URL 링크값만 보관할지 고민이 되었다.
여러 방면으로 알아보던중 외부 스토리지를 사용하는 경우 FireBase의 Cloud Storage를 사용하거나, AWS의 S3를 사용하는 방법이 있는데 둘다 비용이 발생할 수 있는 서비스라서 선뜻 내 프로젝트에 적용하기에 꺼려졌다. 왜냐하면 최소한의 금액만을 가지고 개발하기를 원했고, 개발 초기부터 큰돈을 써가면서 개발하는게 큰 의미가 없다고 생각했기에 비용적인 부분을 생각하지 않을 수가 없었다. 또한 내가 사용자에게 적용하고자 하는 서비스는 이미지를 쉽게 복사/다운로드 할 수 있어야 하기에 외부 스토리지를 사용하는 경우 트래픽에 따른 비용이 어느정도 발생할지 짐작 조차 되지 않았기 때문에 더욱더 신중해 질 수 밖에 없었다.
결론적으로 위의 생각이 머리속에 가득차버리니, 어차피 큰 프로젝트 하는것도 아니고 사용자에게 이미지를 보여주고 복사/다운로드 기능을 제공하는것에 있어서 이미지 원본 URL 값만 알고 있어도 충분히 제공할 수 있을거라고 생각하였고, 만약 이 프로젝트가 차후에 커진다면 그때는 디비상에 존재하는 이미지 URL 값을 외부 스토리지에 이미지 자체를 저장하고 서비스를 제공하면 그만이라고 생각하여서 이 시점에서의 위의 질문의 답은 결국 이미지 URL 값을 DB에 저장하는 것으로 일단락 되었다.
EC2의 인스턴스 유형은 t2.micro
스프링 부트 서버를 구성한 것을 EC2에 배포하고자 하였다. 내가 잠을 자는 동안에도 알아서 서버가 동작하고 이미지를 가져오고 DB상에 저장을 해두어야 프론트에서 새로운 이미지를 계속해서 노출시킬 수 있을테니까 가장 먼저 하였던 작업이 EC2 인스턴스 생성 이었다. 최근에 AWS 강의를 들어서 EC2는 굉장히 빠르게 생성하였고 블로그에 기록해두었던 공부 내용이 빛을 내는 순간이 되었다. 그렇게 생성한 인스턴스에 GitHub에 저장한 소스를 Ubuntu 환경에 Git Pull을 진행하고 Build를 진행하였다.
Build는 생각보다 리소스를 크게 잡아먹는다.
일이 일사천리로 진행이 되고 있던 시점에서 이상한 현상을 목격하게 되었다. 스프링부트 프로젝트의 소스를 빌드하는 시점에서 자꾸 인스턴스가 멈춰버리는 현상이 발생하였다. 아예 먹통이 되어서 좀비마냥 반응이 없는 상태가 되어서 3-4번 정도 인스턴스를 삭제하고 재생성 하는 과정을 겪을 즈음에 갑자기 그런 생각이 들었다. 혹시 t2.micro의 인스턴스 유형의 스펙으로는 감당하지 못하는 작업인건가? 바로 구글에 검색을 진행 해보게 되었고 나와 같은 현상을 겪는 사람들이 굉장히 많다는 것을 알 수 있었다.
(흔한일인듯 했다.)
특히 https://rebugs.tistory.com/654 이분의 글을 통해 멈추는 이유와 해결방법에 대한 인사이트를 얻을 수 있었는데 t2.micro는 1개의 cpu와 1GB의 RAM을 제공하기에 스프링부트 프로젝트 Build시에 많은 메모리를 요구하게 되면서 Build가 실패하는 것으로 결과가 이어지게 되는 것이었다. 해결방법으로는 크게 두가지가 존재하였는데 첫번째는 인스턴스 유형을 업그레이드 하는것이다. 기존의 t2.micro 보다 더 좋은 사양으로 업그레이드 하면 빌드시에 필요한 메모리가 충족되면서 문제가 해결될수 있다. 두번째로는 swap을 사용하는 것인데, 시스템의 물리적 메모리(RAM)이 부족할 때 하드 디스크의 일부를 가상 메모리로 사용하는 것이다. 이렇게 하면 RAM이 부족할 때 시스템이 멈추는 대신 디스크를 사용하여 추가적인 메모리를 확보하여 작업을 완료 할 수 있다. 나는 첫번째 방법보다는 두번째 방법인 swap을 통해 문제를 해결하기를 원했고 EC2 Ubuntu 환경에 swap 설정을 하니, 빌드가 정상적으로 실행되는 것을 확인할 수 있었다.
SCP 방식의 CI/CD PipeLine 구축
스프링부트 코드도 잘 짯고, EC2에 빌드도 완료하여 구동도 완료하니, 자동화 과정이 필요하다고 생각했다. 즉 PipeLine에 대한 구축이 필요하다고 느겼다. 최근에 공부했던 SCP방식을 적용하고자 하였고, 프론트 소스는 어차피 깃허브 페이지에서 호스팅 되고 있었기에 구축이 필요없었고 백엔드 소스만 구축이 필요했다. 딱 이시점에서 1주일전에 공부하였었던 내용이었고 이렇게 빠르게 적용하게 될줄은 몰랐었는데 이때 작성하였었던 글의 내용을 참고하여 GitHub Actions로 PipeLine을 구축하였다.
(빌드파일만 전달하는것임에도 1분 30초가 걸린다.)
다음으로
프론트 소스는 깃허브 페이지에 호스팅하며, EC2에서 크롤링 서버가 돌아가며 데이터를 FireBase에 저장한다. 프론트는 FireBase에 저장된 데이터만 사용자 화면에 보여주는 간단한 구조를 가지고 있다. 다만 프론트 소스에서 FireBase DB에 직접 접근해야 하기에 js 파일에는 파이어베이스 키값이 하드코딩 되어 있어 보안적으로 위험 하였으며, 프론트가 DB에 직접 접근하는 이상한 구조가 마음에 들지 않았다. 또한 RDB가 아닌 FireBase를 사용하고 스프링부트에서 제어하려고 하니 굉장히 귀찮았다. 쿼리를 편하게 작성할 수 있는것도 아니고, 내가 기존에 하던 방식으로 데이터를 관리하는 것이 아니다보니, 시간이 지날수록 데이터가 많아짐에 따라 점점 버거워 지기 시작했다.
이러한 문제들을 해결하기 위해 보다 안전하고 효율적인 데이터 관리 방안을 모색하는 것이 필요하다고 느꼈다. Firebase의 편리함과 실시간 데이터 처리 능력은 매력적이지만, 보안과 확장성 측면에서 한계가 분명히 보였다(특히 섬세한 쿼리 조작이 불가하다). 앞으로는 RDBMS와 같은 전통적인 데이터베이스 시스템을 도입하고, 백엔드 서버를 통해 데이터 접근을 관리함으로써 보안성을 강화하고 데이터 관리의 효율성을 높이는 방안을 고려할 예정이다. 또한, 기존의 스프링부트 환경을 최대한 활용하여 개발 효율성을 유지하면서도 안정적인 서비스를 제공할 수 있는 구조를 구축하는 과정을 다음 글에 작성하고자 한다.
feat. 구입한 도메인을 EC2에 연결하고 무료로 https 적용하기
-
[diary] 이번주는 정말로 시간이 너무 부족했던 사람
intro : 아 이번주 이상하게 일이 흘러가서 시간이 없네
아, 항상 이 diary 글은 매주 토요일마다 작성하는게 사실상 국룰이었는데 어쩌다 보니 한주를 마무리하는 일요일 늦은 저녁에 글을 작성하게 되었다. 이번주는 일단 월요일부터 회고를 시작해야한다.
이번주 월요일은 내가 그토록 이루고 싶어하였던 백준 골드 티어를 달성하였다. 기쁨의 눈물을 흘리지 않을수가 없었는데 정말이지 기뻐서 춤까지 췄다.
(기분 찢어지는줄 알았다.)
골드를 달성한게 기분이 좋은것도 있지만, 내가 목표하였던 것에 대해서 지치지 않고 꾸준히 노력해서 달성한것이 너무 기뻣다. 물론 아직 공부는 한참 더해야겠지만 그래도 어디가서 백준 요즘 풀고있다고?는 말할수 있을것 같다. 티어낮으면 어디가서 한다고 말하기도 조금 부끄러울거 같더라.
(쿡쿡 난 이제 브론즈나 실버따위가 아니라고!)
백준티어 달성하고나니 리눅스 시험도 치뤘겠다, 백준도 달성했겠다. 이번년도 시간도 얼마 남지 않은 시간에서 무언갈 목표하고 하기에는 애매하다고 생각해서 편하게 그동안 하지못하였던 부가적인 공부 특히 3개월정도 김치마냥 숙성시켜둔 테스팅 강의라던지. html/css 강의라던지, 정말 구매만 해두고 하나도 안들은 강의들이 몇개 숨어있었는데 그걸 듣기로 결정했다.
(남는 시간 편하게 보내자는 마인드.)
그래서 웃으면서 한쪽에는 예능 틀어놓고 한쪽에는 강의 틀어두고 대충대충 듣고있었는데, html과 css 강의를 듣다보니 이거는 뭔가 듣는게 크게 도움이 되지는 않는거 같고 해봐야 알수 있는 것들이 굉장히 많았다. 예전부터 css의 레이아웃 부분이 굉장히 약하기도 했고 아직도 이해가 잘 안가는 부분이 많았는데 이건 직접 무언가 결과를 만들어보아야 알 수 있을 것 같아서 갑자기 목표심이 불타? 올랐다.
그래서 뭘 만들지?.. 고민을 하다가 레이아웃 뭔가 백준마냥 문제풀면서 감을 먼저 잡아볼까? 하고 다음과 같은 사이트에서 문제를 냅다 풀어봤다. css문제는 백준마냥 어렵지도 않고 직관적이서 생각보다 재밌었다. 혹시 이런쪽으로 공부를 해야한다면 오히려 이렇게 공부하는게 효율적인거 같다.
레이아웃연습1
(레이아웃 연습하기 좋다 1)
레이아웃연습2
(레이아웃 연습하기 좋다 2)
레이아웃연습3
(레이아웃 연습하기 좋다 3)
레이아웃연습4
(레이아웃 연습하기 좋다 4)
특히 1번과 3번은 정말 재미있었다. 개인적으로 저런 사이트 만드는 분들 리스펙한다. 도움도 되고 정말 배우는것도 많았다. 쉬워서그런가?. 하여튼 저거 문제좀 풀어보니까 레이아웃에 대해서 자신감이 갑자기 확 생겨서 아니 그냥 깃허브 페이지에다가 정적 페이지로 뭐 하나 만들어볼까? 어차피 호스팅 비용 무료인데 연습하는 겸해서 하나 해봐야겠다… 했던게 일이 커져서 이렇게 되었다.
(사이트 홈화면 상단)
(사이트 홈화면 하단)
아이디어는 뭐 별거없이, 그냥 내가 좋아하는 무도짤을 랜덤으로 보여주는 사이트를 만들고 싶었다. 아주간단하게 버튼을 클릭하면 도박판의 슬롯머신이 돌아가는느낌으로 랜덤한 사진이 하나 나오고.. 요런 느낌인데 결국에 구현하기는 했다. 여기서 멈췄으면 좋았는데. 갑자기 방명록 페이지를 만들고 싶어졌다. 뭔가 누군가가 내 페이지에 들어왔을때 글을 남겨주면 좋을거 같아서 만들수있나? 싶기도 했는데 세상에 안되는건 없더라.
(방명록 페이지)
정적 페이지라서 누군가 글을 남기면 재 배포하거나 새로고침 하면 사라지는데 그게 싫어서 좀 가벼운 DB 없나? 무료로 쓸수있는게 있으면 좋을텐데, AWS는 전부다 돈이어서 쓸줄 알아도 애초에 리스트에서 없었는데 갑자기 FireBase가 떠올랐다, 몇년전에 가볍게 무료로 사용할수 있었던게 기억이 나서 지금도 그런가? 하고 보았는데 여전히 어느 한도 내에서는 무료로 사용할수 있었고 어차피 사용랑이 많지 않은 사이트를 구성하는거라서 적당하겠다 싶어서 FireBase DB를 연동하였다.
(처음에는 레이아웃 연습하자고 했던게 FireBase db 까지 연동을 해버렸다.)
여기까지 만들고보니 무한도전 이미지를 크롤링하는 서버가 있으면 좋을거 같다는 생각이 들었다. 왜냐하면 다양한 이미지중에 랜덤으로 사용자에게 보여주면 좋지 않을까? 라는 생각이었는데 셀레니움으로 구글 이미지를 크롤링하는건 너무 힘들거 같아서 구글에서 합법적으로 제공해주는 Google Custom Search Engine 을 연동해서 서버에서 이미지를 크롤링해서 FireBase에 저장해두고 해당 이미지를 깃허브 정적페이지에서 DB의 이미지를 불러와서 화면상에 렌더링 해주면 되겠다 싶었다.
(사실 FireBase 연동하는거 까지했을떄 이미 난 시스템종료였는데...어쩌다 여기까지 왔을까 나도 모르겠다.)
그래서? 만들었다. 정말로. 서버를 EC2에 배포하여 CI/CD 자동화 구축도 해두고 기존에 배웠던 전략들을 전부 적용했고 현재도 스케줄러에 등록된 작업들이 돌아가면서 FireBase DB를 최신화 시키고 있다, 말나온김에 지금 뭐하고 있나 볼까?
(음 뭐 대충 랜덤 이미지 데이터를 성공적으로 업데이트 한거 같다.)
더 이야기를 풀고싶은데 그건 나중에 project 카테고리 글을 작성할때 조금 더 자세히 기록해두어야 겠다. 어쩌다 보니 토이 프로젝트? 정도 규모의 사이즈가 되어버려서 기록을 안할수가 없을거 같다. 나름 고민을 많이한 포인트도 있고 개발하면서의 희노애락이 조금 있어서 블로그감인 부분이 몇개 있다. (더 자세한 내용은 나중에 project 카테고리에 정리할 예정!)
(진짜 살다보니 내가 대충 개발 해도 포폴이 될수있는 날이 오네 ?)
몇가지 좀더 기능 확장이 필요한 부분이 있어서 아직 개발중이긴 하지만 이왕 서버까지 돌리는 김에 좀더 사이트가 컨셉을 가지고 서비스를 제공하는 쪽으로 방향을 틀 생각이다. AWS에서 고정 IP 설정한게 돈이 아까워서라도 뽕을 뽑아야겠다. 정말로.
지금까지 개발하는 목요일부터 일요일까지 3~4일 정도 걸렸는데 생각보다 빠른 시간인거 같기도 하고 느린거 같기도 하고 흠.. 개인적으로는 일단 만족이다.
개발 이야기는 여기까지하고, 운동이야기를 조금 해야겠다. 뭔가 요즘 앉아있는 시간이 많아지다보니 하체가 너무 약해지는거 같다. 운동을 아침 저녁으로 나눠서 매일 루틴으로 할 수 있도록 고정시켜야 할거 같다. 이번주도 은근히 운동을 많이 못한거 같아서 아쉽달까. 저번주에도 똑같은 소리했던거 같은데 매주 같은 후회를 하다니. 칠칠치 못한거 같다.
(운동은 선택이 아닌, 필수.)
뭔가 이번주에는 재미있었던 일이 있었다기 보다는 의외로 개발에 크게 집중해서 몰입했던 시간이 대부분 이었던거 같다. 하루종일 개발하다가 잠들기전에 GPT한테 아이디어 확인하고 잠들고, 뭐 거의 그 반복이었던거 같다. 그나저나 GPT가 처음에는 좀 멍청하고 별로라고 생각했었는데 별로라고 생각하는건 여전하면서도 쓸모가 굉장히 있는거 같다. 방향성을 정하기 위해서 선택지 제공을 너무나 잘 해주는거 같다. 도구는 활용하기 나름이라더니 쓰다보니까 이제는 없으면 안될 존재가 되어가고 있다.
(GPT 너 요즘 좀 괜찮아 보인다?)
이번주도 마무리가 되어가고 다음주면 이번년도도 끝이다. 나름대로 이번년도 마무리를 잘 하고 있는거 같아서 스스로에게 좀 기특한거 같다. 시간을 허투로 보내지는 않는거 같으니 다행이다. 아 그리고! 다음주 큰 이슈가 있다.
(오~ 1차는 통과구만!)
자 그럼 다음주에 예정되어있는 역량검사를 준비해야하는데 1기 합격생 분들의 후기를 조금 찾아보니까 2문제로 제출이되고 각 문제에 소단위 문제로 5문제씩 발전해 나가는 형식의 문제라고 한다. 어떤식의 출제인지 난이도는 어떤지 감이 잘 오지는 않는데 아무래도 코딩테스트의 느낌일테니 준비가 좀 많이 필요해보인다. 다음주는 아무래도 코테 문제만 계속 풀면서 감을 살려둬야 할 것 같다. 별생각없이 그냥 IQ 검사 이런거 하는건줄 알고 시험일자도 가장빠른날자에 빠른 시간대로 1빠로 하고 치워야지 했는데 잘못생각했다. 좀 자세히 알아보고 결정할걸.
(다음주에 진짜 내가 이러고 있는거 아닌가 모르겠다.)
하여튼… 빠르게 지나간 이번주의 기록은 여기까지 남겨두기로 하고. 일요일의 마지막을 웃으면서 보낼수 있도록 운동이나 조금 더 하다가 잠이나 자야겠다. 한 10분만 더해야지.
음 글은 여기까지인데 어떻게 마무리 하더라. 오늘은 다른짤로 마무리 해야지.
다음주를 향해 출바알~~~~~~~~~
(명수형 짤이 은근 재밌는게 많단 말이지.)
-
[pipeline] 백엔드(spring boot) 프로젝트에 다양한 ci/cd 방식 적용하기
intro : 백엔드(spring boot) 프로젝트에 다양한 ci/cd 방식을 적용해보자.
하기 글은 다음과 같은 과정이 선행되어야 합니다. 다른 글들에 비해 생략된 과정이 많습니다.
[aws] ec2를 통해 백엔드 api 서버 배포하기 (1)
[aws] ec2를 통해 백엔드 api 서버 배포하기 (2)
[pipeline] ci/cd와 github actions의 기본 개념
개인 프로젝트에서 많이 쓰는 CI/CD 구축 방법
이 방법은 주로 개인 및 소규모 프로젝트에서 CI/CD를 심플하고 빠르게 적용시키고 싶을 때 사용한다. 해당 방식은 다음과 같은 장점과 단점을 가진다.
(전체적인 흐름)
장점
git pull을 활용해서 변경된 부분의 프로젝트 코드에 대해서만 업데이트 하기 때문에 CI/CD 속도가 빠르다. 대부분의 CI/CD 방식들은 전체 프로젝트를 통째로 갈아끼우는 방식을 사용한다. CI/CD 툴로 Github Actions만 사용하기 때문에 인프라 구조가 복잡하지 않고 간단하다.
단점
빌드 작업을 EC2에서 직접 진행하기 때문에 운영하고 있는 서버의 성능에 영향을 미칠 수 있다. Github 계정 정보가 해당 EC2에 저장되기 때문에 개인 프로젝트 또는 믿을만한 사람들과 같이 진행하는 토이 프로젝트에서만 사용해야 한다.
CI/CD를 적용하기 전의 과정
CI/CD를 도입하기 전의 배포 과정을 살펴보자. 일반적으로 AWS EC2를 사용하는 환경을 가정하면 기본적인 아키텍처와 배포 절차는 다음과 같다.
1. 코드 Push : 개발자가 GitHub의 Main Branch에 코드를 Push 한다.
2. 소스 Pull : AWS EC2에 SSH로 접속한 후, GitHub에서 최신 Main Branch 코드를 Pull 한다.
3. 빌드 및 실행 : EC2 내에서 코드를 빌드하고 애플리케이션을 재 실행 한다.
이 과정의 가장 큰 문제는 최신 코드를 배포할 때마다 개발자가 직접 EC2에 접속하여 수작업으로 Pull, 빌드, 실행을 반복해야 한다는 점이다. 이는 번거롭고, 효율성이 떨어지며, 실수 발생 가능성도 높다. 이러한 문제를 해결하기 위해 우리는 CI/CD를 도입하여 배포 프로세스를 자동화할 것이다.
하기 단계를 진행하기전에 반드시 진행해야 합니다.
sample 프로젝트 로컬에 설치
해당 프로젝트 AWS EC2에 직접 Git Pull 받고 Build하여 실행
정상적으로 Public IP로 접근되는지 확인하기
GitHub Actions에서 EC2에 직접 접근 후 빌드 및 실행
주의사항
AWS EC2 구축시에, 인스턴스 유형으로 t3a.small을 선택하여 구성하자, 프리티어의 t2.micro는 빌드시에 엄청난 리소스에 서버가 먹통이돤다.
Step1. sample 프로젝트 다운로드
기존의 단점이었던 문제를 해결하기 위해서 GitHub Actions에서 AWS의 EC2에 SSH로 접근하여 빌드 후 실행하는 과정을 yml파일로 작성해 보자. 서버파일은 sample 프로젝트를 사용하는것을 권장한다. 해당 프로젝트를 로컬에 다운받고 먼저 실행해 보자. 별다른 문제없이 다음과 같은 화면이 나온다면 문제없이 실행된 것을 알 수 있다.
(정상실행 화면)
Step2. sample 프로젝트 deploy.yml 파일 생성
sample 프로젝트 루트에 .github/workflows 폴더를 생성하고 deploy.yml 파일을 생성한다. 그렇다면 다음과 같은 구조가 되어있을 것이다.
(정상실행 화면)
Step3. deploy.yml 파일에 코드 작성
이어서 deploy.yml 파일에 다음과 같은 코드를 작성한다.
{% raw %}
name: Deploy To EC2
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: SSH로 EC2에 접속하기
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.EC2_HOST }} # EC2의 주소
username: ${{ secrets.EC2_USERNAME }} # EC2 접속 username
key: ${{ secrets.EC2_PRIVATE_KEY }} # EC2의 Key 파일의 내부 텍스트
script_stop: true
script: |
cd /home/ubuntu/api-server-pipeline # 여기 경로는 자신의 EC2에 맞는 경로로 재작성하기
git pull origin main
./gradlew clean build
sudo fuser -k -n tcp 8080 || true
nohup java -jar build/libs/*SNAPSHOT.jar > ./output.log 2>&1 &
{% endraw %}
secrets 값 설정은 아래 공간에서 해야 합니다
(secrets 값 설정)
Step4. Code를 GitHub에 Push 하기
위 단계에서 deploy.yml 파일에 코드를 작성하였는데, 해당 코드를 push한다. 그럼 GitHub Actions 탭에서 다음과 같은 화면을 확인 할 수 있다.
(정상적으로 GitHub Actions으로 빌드 및 배포가 된 모습1)
(정상적으로 GitHub Actions으로 빌드 및 배포가 된 모습2)
이 방법의 단점
가장 처음에는 AWS EC2의 유형으로 t2.micro를 사용하여 똑같은 deploy.yml파일의 설정으로 CI/CD를 진행해 보았는데, 서버가 먹통이 되었다. 아무래도 빌드하는 과정이 생각보다 리소스를 크게 잡아먹는듯 하여 타임아웃 오류가 났는데, 인스턴스 유형을 t3a.small 변경하고 재 진행하여보니 정상적으로 실행된 것을 확인할 수 있었다. 또다른 해결책으로는 swap을 통해서 메모리 사용량을 늘리는 방법이 있다고 한다. 무료 인스턴스 유형으로 해결하기에는 좀 버거워 보인다.
일반 프로젝트에서 많이 쓰는 CI/CD 구축 방법
이 방법은 주로 현업에서 초기 서비스를 구축할 때 이 방법을 많이 활용한다. 처음 서비스를 구현할 때는 대규모 서비스에 적합한 구조로 구현하지 않는다. 확장의 필요성이 있다고 느끼는 시점에 인프라를 고도화하기 시작한다. 왜냐하면 복잡한 인프라 구조를 갖추고 관리하는 건 생각보다 여러 측면에서 신경(비용)쓸 게 많아지기 때문이다. 해당 방식은 다음과 같은 장점과 단점을 가진다.
(전체적인 흐름)
장점
빌드 작업을 Github Actions에서 하기 때문에 운영하고 있는 서버의 성능에 영향을 거의 주지 않는다.
CI/CD 툴로 Github Actions만 사용하기 때문에 인프라 구조가 복잡하지 않고 간단하다.
단점
무중단 배포를 구현하거나 여러 EC2 인스턴스에 배포를 해야 하는 상황이라면, 직접 Github Actions에 스크립트를 작성해서 구현해야 한다. 직접 구현을 해보지는 않았는데 꽤 복잡하다고 한다.
하기 단계를 진행하기 전에 위 단계를 꼭 거쳐주세요
lsof -i :8080 // 8080 포트로 실행중인 프로세스 찾기
kill -9 PID // pid 값에 8080 포트로 실행중인 프로세스 종료하기
rm -rf api-server-pipeline // 기존에 EC2에 설치되어있는 sample 프로젝트 삭제하기
GitHub Actions에서 빌드 후 EC2에 빌드 파일 실행
기존에는 GitHub Actions의 역할이 EC2에 접근하여 직접 빌드 및 실행 하는 역할이었다. 이번에는 EC2에서 빌드하는 것이 아닌, GitHub Actions에서 직접 빌드하고, 빌드된 파일을 EC2에 전달만하여 빌드파일을 EC2는 실행만 하는 역할로 CI/CD를 구축해볼 것이다.
Step1. deploy.yml 파일 수정
deploy.yml 파일을 다음과 같이 수정한다. 중요한 부분은 SCP를 통해서 GitHub Action에서 빌드한 파일을 EC2에 전달한다는 점이다.
{% raw %}
name: Deploy To EC2
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Github Repository 파일 불러오기
uses: actions/checkout@v4
- name: JDK 17버전 설치
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: 테스트 및 빌드하기
run: ./gradlew clean build
- name: 빌드된 파일 이름 변경하기
run: mv ./build/libs/*SNAPSHOT.jar ./project.jar
- name: SCP로 EC2에 빌드된 파일 전송하기
uses: appleboy/scp-action@v0.1.7 # 빌드 파일 전송 라이브러리
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
source: project.jar
target: /home/ubuntu/api-server-pipeline/tobe
- name: SSH로 EC2에 접속하기
uses: appleboy/ssh-action@v1.0.3 # SSH 접속 라이브러리
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script_stop: true
script: |
rm -rf /home/ubuntu/api-server-pipeline/current
mkdir /home/ubuntu/api-server-pipeline/current
mv /home/ubuntu/api-server-pipeline/tobe/project.jar /home/ubuntu/api-server-pipeline/current/project.jar
cd /home/ubuntu/api-server-pipeline/current
sudo fuser -k -n tcp 8080 || true
nohup java -jar project.jar > ./output.log 2>&1 &
rm -rf /home/ubuntu/api-server-pipeline/tobe
{% endraw %}
Step2. Controller 파일의 return 값을 수정
정상적으로 반영되었는지 확인을 하기위헤서 return 값을 수정해보자.
package com.example.api_server_pipeline.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AppController {
@GetMapping("/")
public String index() {
return "Hello World, GitHub Actions CI/CD Success! + 변경하였습니다!";
}
}
Step3. 변경된 파일들을 git push 하고 결과 확인
수정된 deploy.yml 파일과 Controller 파일을 git push 하여 정상적으로 EC2에 빌드하여 실행되었는지 확인해보자.
(GitHub Actions에서 deploy.yml 파일이 정상적으로 실행된 모습 1)
(GitHub Actions에서 deploy.yml 파일이 정상적으로 실행된 모습 2)
SCP를 통한 CI/CD 구축
위 방법은 굉장히 개인적으로 진행하는 서비스에 당장 적용해보고싶은 방법이다. EC2 성능에 영향을 미치지도 않고, 빌드도 GitHub 에서 진행하는거라서 부담도 없는거 같다.
-
[pipeline] ci/cd와 github actions의 기본 개념
intro : ci/cd의 개념에 대해서 이해하고, ci/cd의 대표적인 github actions 방식에 대해서 알아보자.
하기 글에 대해 참고할수 있는 sample project 입니다.
sample project click!
CI/CD란?
CI/CD는 Continuous Integration, Continuous Deployment의 약어로, 지속적인 통합과 지속적인 배포의 의미를 담은 단어이다.
CI/CD를 왜 배워야 할까?
우리가 어떤 웹 서비스(서버)를 AWS EC2에 배포하고, 특정 기능을 개발 중이라고 가정해보자. 이 경우 보통 다음과 같은 과정을 거치게 된다. 먼저, 새로 개발한 기능을 GitHub에 push한 뒤 EC2에 접속하여 main 브랜치의 최신 코드를 가져오기 위해 git pull을 실행한다. 이후 build와 실행 과정을 거쳐 변경된 서비스를 배포하게 된다.
이 과정이 코드 수정이 발생할 때마다 반복적으로 수행된다고 생각해보자. 상당히 번거롭고 비효율적이지 않은가? 이런 비효율적인 과정을 해결하고자 CI/CD라는 개념이 탄생했다.
CI/CD는 코드 변경 사항을 테스트하고, 빌드 및 배포 과정을 자동화하여 개발 생산성을 크게 향상시킨다. 이를 통해 개발자는 반복적인 작업에서 벗어나 본질적인 개발 업무에 집중할 수 있다. 특히, 빠른 배포와 안정성을 유지해야 하는 현대 소프트웨어 개발 환경에서는 필수적인 기술이라 할 수 있다.
(CI/CD의 일반적인 과정)
CI/CD 흐름을 이해하기 위한 Github Actions 개념
Github Actions는 로직을 실행시킬 수 있는 별도의 컴퓨터라고 생각하면 된다. CI/CD 과정에서 Github Actions는 빌드, 테스트, 배포에 대한 로직을 실행시키는 역할을 하게 된다.
Github Actions을 통한 CI/CD 전체 흐름
CI/CD의 구성방식은 다양하지만 일반적으로 다음의 흐름을 거친다. 개발자 GitHub애 커밋 > GitHub Actions의 Event Trigger 실행 (commit 감지) > AWS EC2로 배포
(전체 흐름)
Github Actions 기본 문법 및 사용법 정리
Step1. 프로젝트 생성 및 폴더, 파일 생성
가장 먼저 Intelij에 Empty Project를 생성한다. 프로젝트명은 다음과 같이 github-actions-study로 지정하였는데, 본인이 원하는 프로젝트 명으로 설정하여도 무방하다. 프로젝트 루트 경로에 .github 라는 이름의 폴더를 만들고 해당 폴더안에 workflows 라는 폴더를 만들어보자. 그 뒤에 deploy.yml 파일을 생성하자.
폴더 및 파일 생성시 주의 사항
.github > workflows 의 폴더 이름들은 철자가 틀리면 안된다. 사용자가 원하는 이름으로 변경해서는 안되고 꼭 지정된 이름으로 생성해 주어야 한다. 다만 확장자 .yml 파일의 이름은 사용자가 원하는 이름으로 생성하여도 무방하다.
(주의 사항 잘 읽고 폴더와 파일을 생성하자.)
Step2. deploy.yml 코드 작성하기
deploy.yml 파일에 다음과 같이 코드를 작성해보자.
name: GitHub Actions 실행시켜보기
on:
push:
branches:
- main
jobs:
My-Deploy-job:
runs-on: ubuntu-latest
steps:
- name: Hello World 출력
run: echo "Hello World"
Step3. github에 프로젝트 push 하기
그 뒤에, github에 repository를 생성하고 push 해준다. 그 뒤에 github 페이지의 Actions tab으로 이동해보면 검정색 화면에서 무언가 실행되고 있는것을 확인할 수 있다. 물론 너무빠르게 실행이 되어서 완료가 된 화면만 볼 수도 있다.
(github-actions-study 프로젝트가 github의 repository에 잘 저장되었다.)
(yml 파일에 입력하였던 echo "Hello World"가 잘 실행된 모습.)
지금까지의 과정속의 deploy.yml 파일의 코드 해석하기
deploy.yml 파일의 최상단 name 부분을 보면 GitHub Actions 실행시켜보기 라고 작성하였었다. 이 부분은 Github Actions에서 workflows의 이름이 된다.
(코드를 확인하세요)
(name: GitHub Actions 실행시켜보기)
on push ~ main의 해당하는 코드 부분의 내용은 해석하자면, 브랜치명이 main에 push가 일어난다면, yml파일에 작성한 로직(jobs)을 실행하겠다 라는 뜻이다.
(코드를 확인하세요)
이어서 jobs ~ run의 해당하는 코드 부분의 내용을 해석해보자.
(코드를 확인하세요)
가장 먼저 jobs에 대해 짚고 넘어가자. 하나의 workflow는 반드시 1개 이상의 job으로 구성되어야 한다. 만약 여러 개의 job으로 구성된 workflow라면, 이들은 기본적으로 병렬적으로 수행된다. 그러나 현재 코드는 단 하나의 job으로만 이루어져 있어, 실행 시 이 단일 job만 수행된다.
My-Deploy-Job 부분의 내용은 job을 식별할 수 있는 이름이라고 볼 수 있는데, 다음과 같이 Actions tab에서 이름이 지정되어 있는것을 볼 수 있다.
(job을 식별하는 ID라고도 볼 수 있음.)
runs-on 부분은 어떤 서버(컴퓨터)에서 Github Actions를 실행시킬 것인지를 작성하게 되는데, 보통 이런 OS는 리눅스의 Ubuntu를 사용하고 최신 버전임을 알리는 latest 키워드가 뒤에 붙게된다.
마지막으로 steps에 대해 살펴보자. 하나의 job은 여러 개의 step으로 구성될 수 있다. 마치 하나의 workflow에 여러 개의 job이 포함될 수 있는 것처럼, 하나의 job 안에도 여러 개의 step이 존재할 수 있다. 각 step은 특정 작업을 수행하며, 순차적으로 실행된다.
Step4. deploy.yml파일에 step 추가하기
기존에 작성한 deploy.yml 파일의 내용을 어느정도 알아보았다. 우리는 이번에 step 부분에 여러개의 명령어를 한번에 입력해 볼것이다. 코드를 다음과 같이 수정해보자.
name: GitHub Actions 실행시켜보기
on:
push:
branches:
- main
jobs:
My-Deploy-job:
runs-on: ubuntu-latest
steps:
- name: Hello World 출력
run: echo "Hello World"
- name: 여러 명령어 문장 출력하기
run: |
echo "Good"
echo "Morning"
여러 명령어 문장 출력하기라는 step이 추가되었다. 기존의 step > run과 다른 점은, 이번에는 명령어가 한 줄이 아닌 여러 개로 구성되어 있다는 것이다. 때로는 여러 개의 명령어를 하나의 step에서 실행하고 싶을 때가 있는데, 이 경우 | 를 사용하여 여러 명령어를 하나의 run 블록 안에 작성할 수 있다. 이를 통해 한 step에서 여러 작업을 순차적으로 처리할 수 있다.
Step5. 수정된 deploy.yml파일 push 하기
정말로 step 안의 여러 명령어가 실행되는지 확인해보자. 이전에 결과를 확인했던 것과 같이 github의 Actions의 tab에 들어가서 확인 할 수 있다. 우리가 예상한것과 같이 여러 명령어(echo Good, echo Morning)가 실행이 잘 되었다.
(Good Morning이 잘 출력되었다.)
Step6. GitHub Actions 환경변수 사용해보기
Github Actions 자체에는 저장되어 있는 변수가 있다. 대표적으로 아래 테이블에 존재하는 지정된 예약어가 존재하는데 우리는 그것 중 몇개를 deploy.yml 파일에 작성하여 ehco 명령어로 출력해 볼것이다.
변수
설명
GITHUB_REPOSITORY
리포지토리 이름 (owner/repo) 형태입니다.
GITHUB_SHA
실행 중인 커밋의 SHA 값입니다.
GITHUB_REF
실행 중인 브랜치 또는 태그의 참조명입니다.
GITHUB_EVENT_NAME
워크플로를 실행시킨 이벤트 이름입니다 (예: push, pull_request).
RUNNER_OS
실행 환경의 운영체제입니다 (Linux, Windows, macOS).
코드를 다음과 같이 수정한다.
name: GitHub Actions 실행시켜보기
on:
push:
branches:
- main
jobs:
My-Deploy-job:
runs-on: ubuntu-latest
steps:
- name: Hello World 출력
run: echo "Hello World"
- name: 여러 명령어 문장 출력하기
run: |
echo "Good"
echo "Morning"
- name: Github Actions 자체에 저장되어 있는 변수 사용해 보기
run: |
echo $GITHUB_SHA
echo $GITHUB_REPOSITORY
해당 코드를 수정한 뒤에 git push를 진행해보자. 아마 다음과 같은 값이 Actions tab에서 출력될 것이다.
($GITHUB_SHA, $GITHUB_REPOSITORY 값이 잘 출력된 것을 볼 수 있다.)
참고로 $GITHUB_SHA 값은 우리가 방금 커밋한 commit hashcode 값과 일치한다.
Step7. GitHub Actions Secret 환경변수 사용해보기
이전 단계에서 살펴본 것처럼, GitHub Actions는 자체적으로 제공하는 환경 변수(예: GITHUB_REPOSITORY, GITHUB_SHA 등)가 존재한다. 하지만 이 환경 변수들은 코드 실행과 관련된 정보만 제공되며, DB 비밀번호나 API 키처럼 민감한 값들을 관리하는 용도로는 적합하지 않다. 이를 해결하기 위해 GitHub에서는 Actions Secrets 기능을 제공합니다. Secrets를 사용하면 민감한 데이터를 안전하게 저장하고 워크플로에서 참조할 수 있다.
아래의 이미지를 참고하여 GitHub > Setting > Secrets and variables > Actions 으로 이동해보자.
(secrets and variables로 잘 이동해보자)
그 뒤에 Repository secrets의 New Repository Secret버튼을 클릭해보자.
(New Repository Secret 버튼 클릭)
다음과 같이 Name과 Secret에 값을 입력하였는데, Name은 아무렇게나 내가 인식할수 있는 값으로 입력하고 Secret에는 민감한 데이터 값을 입력하면된다. 나는 테스트용도의 값들이기에 무작위의 값을 입력하였다. 그 뒤 Add secret 버튼을 클릭하여 저장해주자.
(잘 입력해보자.)
자 이제 다시 deploy.yml 파일로 와서 우리가 Secret 값으로 저장한 API_KEY_PASSWORD 값에 대해서 출력해보자. 아래 코드를 참고해서 수정 후에 git push 하여 어떻게 Actions 에서 출력되는지 확인해보자.
주의사항
secret 값을 사용할때는 secrets를 앞에 붙이고 . 후에 Name 값을 입력해야한다.
{% raw %}
name: GitHub Actions 실행시켜보기
on:
push:
branches:
- main
jobs:
My-Deploy-job:
runs-on: ubuntu-latest
steps:
- name: Hello World 출력
run: echo "Hello World"
- name: 여러 명령어 문장 출력하기
run: |
echo "Good"
echo "Morning"
- name: Github Actions 자체에 저장되어 있는 변수 사용해 보기
run: |
echo $GITHUB_SHA
echo $GITHUB_REPOSITORY
- name: 아무한테나 노출이 되면 안되는 값
run: |
echo ${{ secrets.API_KEY_PASSWORD }}
{% endraw %}
출력하고자 하였던 API_KEY_PASSWORD 값이 Actions에서 *** 로 출력된다. 민감한 값을 보안을 지키면서 사용 할 수 있음을 알 수 있다.
(*** 으로 민감한 값이 출력된다.)
다음으로
이렇게 하여 기본적인 CI/CD 개념과 GitHub Actions 사용법을 알아보았다. 다음 단계로 Spring Boot 프로젝트에 CI/CD를 적용하는 실습을 진행해보겠다.
-
-
[aws] s3를 통해 파일 및 이미지 업로드 해보기 (feat. spring boot)
intro : s3를 통해 파일 및 이미지 업로드 하는 방법에 대해서 알아보자.
S3란?
요약하자면 AWS에서 제공하는 파일 저장 서비스이다.
S3를 왜 사용하는걸까 ?
우린 사진이나 동영상과 같은 파일들을 내 휴대폰에 저장하기도 하지만 구글 드라이브나 iCloud에 저장하기도 한다. 내 휴대폰에 저장하기에는 용량이 너무 커서 다른 공간에 따로 저장하고 싶기도하고, 조금만 내 휴대폰에 이미지나 파일들이 쌓이기 시작하면 지저분해 진다. 우리가 이렇게 실생활에서 겪는 고충과 같이 백엔드 서버에 이미지를 저장할 수도 있지만, 이미지가 너무 많이 쌓이다보면 용량을 너무 많이 차지하게 된다. 그래서 S3를 이용하여 별도의 공간을 통해 이미지와 파일을 관리한다.
S3 생성하기
버킷이란?
AWS S3에서의 버킷이라는 단어는 레포지토리 즉 저장소를 뜻한다. 통상적으로 하나의 저장소를 버킷이라고 부른다.
객체란?
ASW S3에서 업로드한 파일을 보고 S3에서는 파일이라고 하지 않고, 객체라고 부른다. 즉 객체는 S3에 업로드 된 파일을 말한다.
Step1. S3 버킷 생성하기
버킷을 만들기 위해서 검색창에 S3라고 검색하고 다음과 같은 페이지로 이동하고 버킷 만들기 버튼을 클릭한다.
(버킷 만들기 버튼 클릭)
버킷 생성 페이지에서 가장 먼저 일반 구성 영역의 버킷 이름을 api-static-files-test로 지정해주었다. 정적인 파일을 올린다는 이름이다. S3 버킷 이름은 AWS 내 전역적으로 고유하기에 유니크한 이름을 설정할 수 있도록 해야한다.
(식별할수만 있으면 되기에 원하는 이름을 작성하여도 된다.)
다음으로 이 버킷의 퍼블릭 액세스 차단 설정 영역으로 이동한다. 모든 퍼블릭 액세스 차단이 기본적으로 체크가 되어 있는데 아래 이미지처럼 언체크로 변경하고, 현재 설정으로 인해 이 버킷과 그 안에 포함된 객체가 퍼블릭 상태가 될 수 있음을 알고 있습니다.에 체크한다.
(퍼블릭으로 열어야지 외부에서 접근하여 다운로드 받을 수 있다.)
다음으로 다른 영역은 기본값에서 변경없이 아래로 쭉 내려서 버킷 만들기 버튼을 클릭한다.
(버킷 만들기 버튼 클릭.)
정상적으로 버킷을 만들었다면 다음과 같이 범용 버킷에 내가 생성한 버킷이 조회된다.
(버킷 생성 완료.)
이후 버킷의 아이디를 클릭하여 권한 Chapter로 이동한다.
(버킷의 권한 Chapter로 이동)
화면을 쭉 내리면 버킷 정책 영역으로 이동할 수 있는데 편집 버튼을 클릭한다.
(버킷 정책 영역으로 이동 후 편집 버튼 클릭)
새 문 추가 버튼을 클릭하여 정책을 추가해주자.
(새 문 추가 클릭)
서비스 선택에서 S3를 검색하고 작업 필터링 부분에 GetObject를 검색하고 체크박스를 선택한다.
(GetObject 체크박스 선택)
아래로 좀 내리면 리소스 추가버튼이 있는데 추가 버튼을 클릭한다.
(리소스 추가 버튼 클릭)
리소스 유형에는 object를 선택하고, 리소스 ARN값에 기본값으로 arn:aws:s3:::{BucketName}/{ObjectName} 나열되어 있을텐데 BucketName에 내가 기존에 생성한 버킷의 이름을 입력하고, {ObjectName}에는 * 값을 입력한다. 그 후 리소스 추가버튼을 클릭한다.
(모든 S3에 업로드된 파일에 접근을 허용할수 있도록 한다.)
그 뒤에 json 형태의 데이터 항목중 Key 값이 Principal 항목의 value 값을 "*" 로 설정한다. 누구에게 이 정책을 허용할 것인지 설정하는 부분인데, 모든 사람에게 허용하기 위해 해당 값으로 입력한다.
(Principal 값을 꼭 설정해주어야 한다.)
그 뒤에 변경 사항 저장을 눌러서 버킷 정책 편집을 완료하자.
(변경 사항 저장 버튼 클릭)
Step2. IAM에서 액세스키 발급 받기
기본적으로 AWS의 리소스에 아무나 접근을 할 수 없다. 그렇기 때문에 S3 접근해서 파일을 업로드 할 수 없다. 하지만 백엔드 서버는 S3에 접근하여 파일을 업로드 할 수 있어야 한다. S3에 접근할 수 있는 권한을 받기 위해서 IAM을 통해 권한을 부여 받아야 한다.
가장 먼저 IAM을 검색하여 해당 페이지로 이동한 뒤, 사용자 생성 버튼을 클릭하자.
(사용자 생성 버튼 클릭)
사용자 세부 정보에 사용자 이름으로 api-server를 입력해주자. 다른 이름을 입력해도 된다. 다음 버튼도 눌러주자.
(사용자 이름 입력)
권한 옵션에 직접 정책 연결선택, 권한정책은 AmazonS3FullAccess을 검색하여 체크 후 다음 버튼 클릭
(AmazonS3FullAccess를 반드시 선택한다.)
사용자 생성 버튼 클릭
(IAM 사용자 생성을 완료하자.)
그 뒤에 우리는 액세스 키를 발급받아야 하기 때문에 위 단계에서 생성한 사용자의 보안 자격 증명 Chapter로 이동한다.
(보안 자격 증명으로 이동하자.)
화면을 좀 내리면 액세스 키 영역이 존재하고 액세스 키 만들기 버튼이 보인다. 해당 버튼을 클릭하자.
(액세스 키 만들기 버튼 클릭)
액세스 키 만들기 페이지에서 사용 사례에 AWS 외부에서 실행되는 애플리케이션 로 선택하고 화면 아래의 다음 버튼을 클릭한다. (차후 과정에서 로컬에 스프링부트를 실행하여 접근할것이기에 해당 옵션을 선택하는 것이다.)
(AWS 외부에서 실행되는 애플리케이션 꼭 선택)
설명 태그 설정 부분은 선택사항이기에 빈칸으로 두고 다음단계로 진행해도 된다. 바로 액세스 키 만들기 버튼을 눌러주자.
(액세스 키 만들기 버튼 클릭 )
이 단계에서 제공되는 액세스 키 값은 현재 페이지 에서만 확인이 가능하다. 유출되지 않도록 어딘가에 잘 저장해서 보관해야 차후 스프링 부트에서 해당 키값을 통해 AWS의 S3 버킷에 접근하여 업로드가 가능하다. 키값을 확인 완료하였으면 완료 버튼을 클릭한다.
(키값을 잘 저장해두자.)
S3와 스프링 부트 연결하여 이미지 업로드 하기
이 단계 에서는 스프링 부트 서버가 필요한데 다음과 같은 sample 프로젝트를 clone하여 액세스 키값만 변경하고 S3와 연동해보자. 가장 먼저 해당 본인 PC에 프로젝트를 clone 하자.
하기 단계는 프로젝트를 PC에 설치 완료하였다는 가정하에 진행됩니다.
Step1. yml 파일에 버킷 이름과 키값 입력하기
application.yml.yml 파일의 버킷이름과 키 값을 확인 후에 입력한다.
(자신의 버킷 이름과 액세스 시크릿 키값을 입력합니다.)
Step2. 프로젝트 실행하기
yml 파일 제외하고 다른 부분들은 변경하지 않고 프로젝트를 실행한 뒤, 8080 포트로 서버가 잘 실행되었는지 로그를 통해 확인합니다.
(8080 포트로 정상적으로 실행중인지 확인필요.)
Step3. Postman을 통해 /upload API 호출
/upload API를 post 메소드로 호출. 이때 postman의 body는 form-data로 key 값을 File 타입으로 변경한 뒤에 키 값입력을 file, value 부분에는 업로드 하고자 하는 파일을 선택해 준 뒤에, Send 버튼을 클릭하자.
(Postman을 통해 API 호출해보자)
정상적으로 이미지가 업로드 된 경우, 리턴값으로 aws에 업로드한 이미지 url 주소값이 리턴된다. 해당 값을 클릭해보면 내가 업로드한 이미지를 확인 할 수 있다.
(리턴값으로 aws s3에 업로드된 이미지 주소가 return 된다.)
또한 업로드한 이미지를 S3 대시보드에서 확인할 수 있는데 다음과 같이 조회된다.
(S3 버킷에 정상적으로 업로드 된 것을 확인 완료.)
S3 종료하기
버킷은 삭제하기전에 객체를 반드시 전부 삭제 해야한다. 먼저 객체부터 삭제한다.
(전부 선택 및 삭제버튼 클릭)
S3 버킷에 객체 전부 선택하고 객체 삭제 버튼 클릭
(객체 삭제 버튼 클릭)
삭제할 버킷 선택 후 삭제 버튼 클릭
(삭제 버튼 클릭)
버킷도 삭제 완료하자.
(버킷 삭제)
다음으로
이렇게 해서 S3를 이용한 파일 및 이미지 업로드 기능을 스프링 부트에 적용하는 작업을 마무리했다. 이제 다음 단계로, S3의 또 다른 기능인 정적 웹사이트 호스팅을 학습하고, 이를 CloudFront와 함께 적용하는 방법에 대해 알아볼 예정이다.
다음글 : [aws] s3와 cloudfront를 활용한 웹 페이지 배포
-
[aws] rds(database) 생성하고 접속하기 (feat. mysql)
intro : rds(database)를 생성하고 접속하는 방법에 대해서 알아보자.
RDS란?
요약하자면 관계형 데이터베이스를 뜻하는데, MySQL Maria DB 등 다양한 관계형 데이터베이스를 AWS로부터 빌려서 사용하는 행위이다.
RDS를 왜 사용할까?
로컬환경에서 개발할때는 로컬환경에 설치된 MySQL와 같은 DB를 연결해서 사용한다. 하지만 서버를 배포하고 나면 서버가 내 컴퓨터에 설치된 MySQL과 연결할 수 없다. DB도 외부 인터넷에서 접속할 수 있도록 같이 배포해 주어야 한다. 이러한 이유 뿐만아니라 여러 부가기능 자동백업 모니터링 등을 가지고 있다.
EC2에 MySQL을 직접 설치하면 안될까?
해도된다. 다만 EC2에는 서버를 배포하는게 통상적인데 서버와 DB를 같은 한 컴퓨터 안에 구성하고 운영을할시에 서버가 죽으면서 DB도 같이 죽는 현상이 발생할 수 있기에 현업에서는 분리하여 서버는 EC2에, DB는 RDS에 설치하는게 통상적이다. 다만 개인프로젝트나 학생이 운용하는 경우 비용절감을 위해 EC2에 DB를 설치하고 운용하는것도 괜찮은 방법이다.
RDS 생성하고 접속하기
Step1. RDS 생성하기
가장먼저 rds를 검색하여 대시보드로 접근한다. 그 뒤 대시보드의 데이터베이스 생성 버튼을 클릭하자.
(굉장히 직관적으로 데이터베이스 생성 버튼이 떡하니 있다.)
데이터베이스 생성 버튼을 클릭하면 다음과 같은 페이지가 보이는데 우리는 MySQL을 생성하고 접근해보겠다. 이미지처럼 MySQL을 선택한다.
(MySQL 선택)
다른 부분들은 변경없이 템플릿 영역으로 이동하여 프리티어를 선택해준다. 프리티어라고 해서 성능적으로 크게 나쁜 유형은 아니라고 한다.
(프리티어 선택)
설정 영역의 DB 인스턴스 식별자를 입력한다. 나같은 경우는 api-db 라고 지정하였다. 그 뒤에 자격증명 설정 영역에 값을 입력해야 하는데 DB의 ROOT 계정의 아이디와 비밀번호를 초기 설정하는것이라고 생각하면 된다. 아이디 admin 비밀번호 본인만 아는값 으로 설정하고 넘어간다.
(자격증명 설정에 작성한 값은 어딘가에 기록해두자.)
다음으론 인스턴스 구성 영역에서 DB 인스턴스 유형을 선택한다. t4g.micro가 프리티어에 해당하기에 선택해 주었다.
(프리티어에 해당하는 유형을 잘 찾아서 선택해주자.)
다음으로는 스토리지인데 유형으로 범용 SSD(gp3)를 선택하고 할당된 스토리지는 20GB를 설정한다. 프리티어는 20GB까지 지원해준다.
(프리티어 범위안에 해당하는 값을 잘 찾아서 선택하자. 스토리지는 20GB)
연결영역에서는 다른 부분 변경하지 않고, 퍼블릭 액세스 부분에서만 예로 선택을 확인해준다. 나중에 외부에서 RDS에 접근할 수 있도록 설정하는 거라고 볼 수 있다.
(퍼블릭 액세스에만 예 버튼을 클릭한다.)
위 과정의 단계를 완료한 뒤에 다른 기본값들은 변경하지 않고 쭉 내려서 데이터베이스 생성 버튼을 클릭해주자.
(데이터베이스 생성 버튼 클릭)
다음과 같이 RDS가 생성되고 있음을 확인 할 수 있다.
(생성이 완료되는데 시간이 조금 걸린다. 5분정도 소요되는 것 같다.)
Step2. RDS 보안그룹 설정하기
생성이 완료되는것을 기다리면서, RDS에 설정할 보안그룹을 생성해야한다. EC2 > 네트워크 및 보안 > 보안 그룹 으로 이동한다.
(보안 그룹 생성 버튼을 클릭하자.)
기본 세부 정보의 보안 그룹 이름을 나같은 경우 api-db-security-group 이라고 작성하였다. 아래 설명도 동일하게 작성해준다. 인바운드 규칙으로는 외부에서 어디서든지 접근할수 있도록 Anywhere-IPv4를 설정해준다.
(3306으로 접근할수 있도록만 인바운드 규칙을 잘 설정하자.)
아웃바운드 규칙은 변경하지 않고 쭉 아래로 내려서 보안그뤕 생성 버튼을 클릭하자.
(보안 그룹 생성을 완료하자.)
보안 그룹을 생성완료하고 난뒤, 다시 RDS로 이동하자. 우리가 아까 생성중이었던 RDS는 생성이 완료되었다고 나올 것이다.
(보안 그룹 생성을 완료하니 RDS도 생성이 완료되어있다.)
이제 우리가 생성한 api-db를 편집하여 보안그룹을 변경해줄것이다. 기존에는 default로 설정되어 있는것을 우리가 방금 만든 api-db-security-group 로 변경하는 작업을 진행한다. 가장먼저 api-db를 클릭하고 수정 버튼을 누르고 들어가서 아래로 쭉 내리면 연결 영역이 보이고, default로 보안그룹이 설정되어 있는데 이것을 아래 이미지처럼 api-db-security-group 로 변경해준다.
(보안 그룹을 변경해주자)
다른 부분들은 변경하지 않고 쭉 화면을 내리면 다음 버튼이 있다. 계속 버튼 클릭해 주자.
(계속 버튼 클릭)
다음으로 우리가 변경한 보안그룹을 즉시 변경으로 선택해주고 DB 인스턴스 수정 버튼을 클릭하자.
(즉시변경 DB 인스턴스에 적용)
보안 그룹이 즉시변경되었음을 알림창을 통해 알 수 있다.
(보안 그룹 변경 완료)
Step3. 파라미터 그룹 추가하기
다음과 같이 RDS의 파라미터 그룹 카테고리로 이동한 후 파라미터 그룹 생성 버튼을 클릭한다.
(파라미터 그룹 페이지)
생성 페이지에서 다음 이미지와 같이 값을 같이 작성한다.
(이미지를 잘 보고 따라서 선택하자)
아래처럼 생성된 파라미터 그룹을 수정해야한다. api-db-parameter-group 을 누르고 편집을 눌러주자.
(일단은 생성된 파라미터 그룹이 잘 생성되었는지 부터 확인하자)
편집을 누르면 다음과 같은 화면이 나온다.
(편집페이지로 들어가면 파라미터들을 변경할 수 있는 값들이 나온다.)
우리는 여기서 character 라고 검색하여 인코딩 방식을 utf8mb4(한글 한자 이모티콘을 모두 지원하는 인코딩 방식이다.)로 변경할것이다. 변경해야 하는 항목을 정확하게 보고 다음과 같이 값을 입력하고 변경 사항 저장 버튼을 클릭한다.
(변경해야하는 파라미터 1)
(변경해야하는 파라미터 2)
다음으로는 다시 편집페이지로 들어가서 collation이라고 검색하고 값을 utf8mb4_unicode_ci 값을 입력할 것이다. collation은 정렬에 대한 규칙을 정의하는 값인데 대중적으로 많이 사용하는 utf8mb4_unicode_ci 으로 값을 입력하고 변경 사항 저장 버튼을 클릭한다.
(변경해야하는 파라미터 1)
(변경해야하는 파라미터 2)
마지막으로 time_zone에 대한 값을 변경한다. Asia/Seoul로 입력하고 변경 사항 저장 버튼을 클릭한다.
(마지막 파라미터 변경이다.)
이제는 우리가 생성하고 변경한 파라미터 그룹을 RDS에 다시 수정해줘야한다. 기존에는 default.mysql8.0 으로 DB 파라미터 그룹이 설정되어 있는데 우리가 생성한 파라미터 그룹이름인 api-db-parameter-group으로 변경한다. 이전에 보안그룹을 수정하였던 것처럼 아래로 내려서 계속 버튼을 클릭하고 즉시 적용으로 파라미터 그룹 변경을 완료한다.
(추가 구성 영역에서 파라미터 그룹을 변경하자.)
지금까지 RDS의 생성 후, 보안그룳 수정, 파라미터 그룹 수정을 완료하였다. 그러나 파라미터 그룹에서의 time_zone의 변경은 RDS의 재부팅이 일어나야지만 적용이된다. 그렇기에 다음과 같이 RDS를 재부팅을 해야한다.
(RDS를 재부팅 해주자.)
재부팅을 완료하였다면 드듸어 RDS에 접속할 준비가 완료된 것이다.
Step4. RDS 접속하기
RDS는 접속하기 위해 여러 툴을 사용할 수 있다. datagrip 이라던지 dbeaver 혹은 intelij 로도 접속할 수 있다. 가장 편한방법으로 접속하면 되는데 intelij로 접속해 보겠다.
가장 먼저 RDS의 내가 생성한 RDS인 api-db를 클릭한다. 페이지가 이동되고 api-db의 여러 세부 정보가 나오는데, 이때 우리가 필요한건 엔드포인트의 주소이다. 이 엔드포인트가 DB 접속시의 HOST 부분에 작성될 값이다.
(엔드포인트를 복사하자.)
인텔리제이를 실행하고 MYSQL DB 연결을 할때 다음과 같이 호스트에 엔드포인트 값을 복사해서 넣어주고 admin 계정과 비밀번호를 입력하여 연결한다.
(RDS MYSQL을 접속해보자)
접속이 잘 된것을 확인하였으면 아래처럼 콘솔창을 열어서 쿼리도 실행해보고 정상적으로 작동하는지 확인해보자.
(간단하게 쿼리작성해서 실행도 해보자.)
RDS 종료하기
위 방법을 통해 RDS를 생성하고 접속하는 방법에 대해서 알아보았다. RDS는 비용이 나가는 기능이기에 사용을 완료하였다면 다음과 같은 방법을 통해 안전하게 종료해보자.
가장 먼저 RDS 대시보드 페이지의 데이터베이스 페이지로 이동한다.
(데이터베이스 페이지)
그 뒤에 삭제하고자 하는 데이터베이스를 선택하고 삭제를 클릭한다.
(삭제 버튼 클릭)
삭제 버튼을 클릭하고 나서 삭제할때의 옵션중에 최종 스냅샷 생성, 자동 백업 보존 항목을 언 체크 하고 삭제를 진행한다.
(언체크 해야 하는 항목 확인이 필요함.)
어느정도의 시간이 지나고 나면 삭제가 완벽하게 처리된다.
다음으로
이렇게 하여 RDS 생성 및 접속하기가 마무리 되었다. EC2에 연결하여 DB 접근을 하는것 또한 포스팅에 포함하고자 하였으나, Intelij에서 DB 접속하는 방법만 알아도 충분히 EC2와 연결하는것은 크게 다르지 않기에 생략하였다. 다음으로는 파일 및 이미지 업로드 (S3)를 학습할 예정이다.
다음글 : [aws] s3를 통해 파일 및 이미지 업로드 해보기 (feat. spring boot)
-
[aws] elb(load balance)에 https 연결하기 (feat. ec2)
intro : elb(load balance)에 https 연결하는 방법에 대해서 알아보자.
이전 글 [aws] route53을 통해 도메인 구매하고 연결하기 (feat. ec2) 와 이어집니다.
해당 글은 하기 과정이 선행이 되어 있어야 합니다.
[aws] ec2를 통해 백엔드 api 서버 배포하기 (1)
[aws] ec2를 통해 백엔드 api 서버 배포하기 (2)
[aws] route53을 통해 도메인 구매하고 연결하기 (feat. ec2)
ELB(Elastic Load Balancer)란?
트래픽을 적절하게 분배해주는 장치로, 전문적인 용어로 로드밸런서 라고도 한다. 서버를 2대 이상 가용할시에는 필수적으로 도입한다. 하지만 이번 ELB를 사용하는 이유는 SSL/TLS를 적용하기 위해 ELB를 사용한다.
SSL/TLS 란?
쉽게 설명하자면 http를 https로 바꿔주는 인증서이다. ssl/tls를 이용하여 http가 아닌 https로 통신할수 있게 만들어 준다.
HTTPS를 적용해야 하는 이유
보안적인 이유
데이터를 서버와 주고받을 때 암호화 시켜서 통신을 한다. 암호화 하지 않으면 누가 중간에 데이터를 가로채서 해킹 할 수도 있다.
사용자 이탈
사이트를 들어갔는데, 보안연결이 사용되지 않았습니다. 라는 문구가 나온다면 믿음직스럽지 못한 사이트라고 생각 할 것이다.
ELB 셋팅하기
Step1. 기본 구성
가장 먼저 aws 콘솔에서의 ec2를 검색해서 들어간 후 로드 밸런서를 클릭하면 아래 이미지의 화면이 보인다. 이때 주황색의 로드밸런서 생성을 클릭하자.
(로드밸런서 생성을 클릭하자)
그럼 다음과 같은 화면이 보이는데, 우리는 https연결을 위한 로드밸런서를 생성 하는것이기에 가장 좌측의 https가 보이는 이미지를 클릭한다.
(가장 좌측의 이미지 https가 있는 이미지를 클릭하자)
이제부터 본격적으로 elb의 설정값들을 작성하게 된다. 로드 밸런서의 이름은 나같은 경우는 api-server-elb로 지정해주었다. 편하게 이름을 작성해도 된다. 체계와 로드밸런서 IP 주소 유형은 기본값으로 설정된걸 변경하지 말자.
(로드 밸런서 이름만 지정하고 나머지는 기본값으로 변경X)
네트워크 매핑은 가용영역에 대한 개념을 알아야 하는데, 하나의 리전에서의 세부 영역을 나누어 관리하는 것을 뜻한다. 다만 우리는 HTTPS를 설정하기 위해 로드 밸런서를 생성하고 있는것이기에 전부 다 선택하여 넘어가면 된다.
(https를 설정하기 위해 로드밸런서를 이용하는것임을 잊지 말자)
여기까지의 단계는 홀딩하고 다음 단계를 넘어가자, 아래의 보안그룹 생성은 새창을 열어서 진행하는 것을 추천한다.
Step2. 보안 그룹
위 단계에서의 네트워크 매핑까지의 작성을 홀딩하고, 보안그룹 생성을 위한 새창을 열고 보안그룹 생성 페이지에 접근하자.
우리는 이전에 ec2에서의 보안그룹을 생성해 본적이 있다. 하지만 로드밸런서 elb에서의 보안그룹은 ec2의 보안그룹과는 다르다. 그렇기에 elb의 보안 그룹을 따로 생성하여야 한다.
(ec2의 보안그룹을 클릭하면 다음과 같은 화면이 나온다)
보안 그룹 생성을 클릭하면 다음 이미지의 화면이 나오게 되고, 이때 보안그룹명을 지정해준다. 식별할수 있도록 나같은 경우는 api-server-security-group으로 작성하였다. (이미지에는 없지만 설명 부분에도 보안그룹명과 같은 값을 입력해 줘야 한다.)
(보안 그룹 명을 지정해 주자, 설명부분에도 같은 값을 입력해 주어야 한다.)
인바운드 규칙으로 http, https의 접근을 허용해준다. (Anywhere-IPv4)
(인바운드 규칙에, http와 https의 요청이 들어오는것에 대한 허용을 지정해준다.)
아웃 바운드 규칙은 건들지 않고, 위 내용을 기반으로 한 보안그룹 생성을 진행한다.
(보안 그룹 생성을 눌러준다.)
위 단계에서 보안그룹을 정상적으로 생성하였다면, 기본 구성 에서의 보안그룹을 우리가 새롭게 생성한 api-server-security-group를 선택할 수 있다.
(보안 그룹에 우리가 만든 보안그룹을 적용한다.)
Step3. 리스너 및 라우팅 / 헬스 체크
elb에 들어온 요청을 어떤 ec2 인스턴스에 요청을 전달할 것인지 설정해야하는데, 그 내용을 바로 리스너 및 라우팅 에서 설정해 주어야 한다. 이때 ec2 어떠한 ec2 인스턴스에 요청을 전달해야할지 대상 그룹을 생성하여야 한다. 아래 이미지에서 대상 그룹 생성 버튼을 클릭하자.
(대상 그룹 생성을 클릭하자.)
대상 그룹 생성 페이지에 들어가서 우리는 인스턴스를 선택하고 이름을 지정해준다. 나같은 경우 api-server-target-group로 설정하였다.
(대상 그룹 이름을 지정하자)
프로토콜 포트 부분에는 로드밸런스에서 받은 요청을 어떤 프로토콜의 포트로 요청을 전달할 것인지 설정하는 부분이다. ec2 인스턴스에 http로 80번 포트로 요청을 전달하자.
(HTTP프로토콜 80번 포트로 전달)
상태검사는 elb 즉 로드밸런서가 여러 ec2에게 트래픽을 전달할 때, 몇초간격으로 ec2 인스턴스의 상태를 확인하고 문제가 없는 ec2 인스턴스에게 트래픽을 전달한다. 만약 요청을 보냈을때 응답이 없다면 elb는 해당 ec2가 문제가 있다고 판단하고 요청을 보내지 않는다. 우리는 지금 elb가 ec2에게 상태를 확인할 수 있는 api의 주소값을 입력하는 것이다. 나는 health라는 api를 ec2에 생성할 것이므로, health라고 이름을 지정하였다. 그 뒤 다음을 누른다.
(상태검사의 개념은 짚고 넘어가자.)
다음 버튼을 누르면 다음의 화면이 나오는데, 실행중인 ec2 인스턴스를 클릭하고 아래의 보류중인 것으로 포함 버튼을 클릭하자.
(실행중인 ec2 인스턴스를 클릭하고, 아래에 보류중인 것으로 포함 버튼을 클릭하자.)
그 뒤에 대상 보기 부분에서 선택한 ec2가 이동 된 것을 확인 후 대상 그룹 생성을 완료하자.
(대상 그룹 생성 완료)
위 방법을 통해 대상 그룹을 생성하였다면, 원래의 리스너 및 라우터를 설정하는 부분에서 위에서 생성한 대상그룹명인 api-server-target-group을 선택해주자.
(대상 그룹 선택 완료)
그 뒤에 나머지 부가적인 설정은 건드리지 않고 로드밸런서 생성 버튼을 클릭하여 완료한다.
(대상 그룹 선택 완료)
로드밸런서 생성 완료를 대시보드에서 확인 후 로드밸런서 이름을 클릭하자.
(로드밸런서 생성 완료)
로드밸런서가 우리의 ec2 인스턴스에 요청을 전달하는지 확인하기 위해 다음과 같이 dns 이름을 복사해서 url로 이동해보자.
(DNS 이름 복사해서 URL 주소창에 입력하자)
다음과 같이 접속된다면 로드 밸런서를 정상적으로 생성하였음을 알 수 있다.
(DNS 이름 복사해서 URL 주소창에 입력하자)
Step4. ELB에 도메인 연결하기
이전에는 도메인을 ec2에 연결했었는데, 위 과정을 통해 로드밸런스에 요청을 보내면 로드밸런서가 자동으로 ec2 에게 요청을 전달하는것을 볼 수 있다. 그렇기에 ec2에 도메인을 연결하는것이 아닌, 로드밸런서에게 도메인을 연결을 하는 과정을 진행한다. 가장 먼저 도메인 연결을 하기 위헤 route53의 호스팅 영역으로 이동한다.
(이전글들에서는 todayvisible로 도메인을 연결했으나, 이번에는 visiblego로 연결한다.)
이전 route53 도메인 관련 작업을 진행할때처럼 동일하게 호스팅 영역의 이름을 클릭하고 레코드 생성을 한다. 레코드 생성페이지에 입력할 값이 많은데 레코드 이름같은 경우 난 www를 붙여서 접근하고싶어서 붙여주었다. 레코드 유형은 그대로 A 타입으로 변경하지않고, 별칭을 반드시 선택해준다. 이전 ec2에 연결할때는 그저 ip주소값만 입력하면 끝이었는데 이번에는 로드밸런서에 연결하는것이라 트래픽 라우팅 영역에 Load Balancer을 선택하고, 리전값은 아시아 태평양 서울 그 아래 로드 밸런서 선택은 우리가 만든 로드밸런서를 선택해준다. (기존에 만든 로드밸런서가 없다면 자동으로 하나만 선택된다.) 이후 레코드 생성 버튼을 클릭하여 마무리 한다.
(레코드 생성버튼까지 눌러주자)
정상적으로 레코드가 생성되었다면 다음과같이 도메인으로 접근시에 이러한 화면을 마주할 수 있다.
(도메인으로 요청시 로드밸런서로 요청전달 후 ec2 요청 전달 성공!)
Step5. HTTPS 적용을 위해 인증서 발급받기
HTTPS의 적용을 위한 기초 단계는 모두 끝이났다. 본격적으로 HTTPS의 적용을 진행해보자. 가장먼저 Certificate Manager을 검색한다 그럼 아래 이미지의 화면이 나오는데, 여기서 인증서 요청 버튼을 클릭하자.
(인증서 요청 버튼 클릭)
퍼블릭 인증서 유형을 선택하고 다음 버튼을 클릭하자.
(퍼블릭 인증서 유형 선택)
도메인 이름만 내가 HTTP로 적용하고자하는 도메인 이름을 작성하고 나머지는 변경하지 않고 요청 버튼을 클릭한다.
(인증서 요청이 완료된 모습이다.)
인증서 요청은 완료되었지만 아직 발급이 완료가 된 것은 아니다. 이 도메인의 주인이 내가 맞다는것을 증명하는 과정을 거쳐야 비로소 인증이 완료가 된다. CNAME 이라는 값을 우리의 호스팅영역의 레코드에 추가를 해줘야한다. 물론 이 CNAME의 이름과 값은 아래 이미지처럼 제공해준다. 편리하게 추가할수 있도록 aws에서는 Route53에서 레코드 추가 버튼을 제공해준다. 버튼을 클릭해보자.
(Route53에서 레코드 생성 버튼 클릭.)
(Route53에서 레코드 생성 버튼 클릭.)
(레코드 생성버튼을 클릭하자.)
정상적으로 CNAMe을 레코드에 추가를 완료하면 다음과 같이 상태가 성공으로 변경된다.
(성공으로 상태가 변경되었다.)
Step6. ELB에 HTTPS 설정하기
드디어 이글의 최종적인 목표인 HTTPS를 적용하는 단계에 도달했다. 우리가 HTTPS를 적용해야 하는 부분은 로드밸런서이다. 도메인도 로드밸런서에 적용했고, HTTPS도 로드밸런서에 적용한다. 모든 요청은 로드밸런서를 통해 트래픽을 ec2에 분배해준다. 자 이제 ec2 대시보드 안에 로드밸런서로 이동하여 리스너를 추가하자.
(일단 로드밸런서로 이동)
그 후 리스너 추가 버튼을 클릭하고 다음과 같은 화면에서 프로토콜에 HTTPS를 선택, 라우팅 액션을 대상 그룹으로 전달, 대상그룹은 르드밸런서를 생성하면서 만들어 두었던 ec2 그룹을 선택한다.
(HTTPS 프로토콜은 반드시 선택한다.)
이후 보안 리스너 설정에서는 이전에 발급받은 도메인 인증서(ACM)를 선택해주는 것만 하면 끝이다. 마지막으로 추가버튼을 눌러주자.
(ACM의 약자는 AWS Certificate Manager 이다.)
리스너 추가를 완료한다면 드디어 HTTPS를 통한 접근이 가능하다.
(여기까지의 과정이 정말 길고 길었다.)
Step7. 리디렉션을 통해 HTTP요청을 HTTPS로 변경하기
근데 좀 아쉬운 부분이 있다. 아직까지 로드밸런서에 HTTP프로토콜의 80포트의 리스너 규칙이 존재하기에 HTTP를 통한 접근이 아직 가능하다. 우리는 이 부분을 리디렉션을 통해 HTTP의 요청을 HTTPS의 요청으로 변경해 줄 것이다. 다음의 과정을 따라가보자.
가장먼저 기존에 등록하였었던 리스너 규칙인 HTTP의 규칙을 삭제한다.
(기존의 HTTP의 리스너 규칙을 삭제한다.)
그 뒤에 다시 리스너 추가 버튼을 클릭하고 다음과 같이 라우팅 액션을 URL로 리디렉션을 선택하고 프로토콜과 포트를 HTTPS, 443을 선택 후 다른 부분은 변경하지 않고 추가버튼을 클릭한다. 그럼 HTTP로 접근하여도 자동으로 HTTPS로 리디렉션이 처리가 된다.
(HTTPS로 리디렉션을 하자.)
(HTTP로 접근하여도 동일하게 HTTPS로 리디렉션이 되어 접근된다.)
ELB 종료하기
HTTPS를 적용하기위해 생성한 ELB(로드밸런서)는 비용이 나가는 기능이다. 사용을 완료하였다면 안전하게 종료해보자. 다음과 같이 ec2의 로드밸런서를 클릭하고 현재 작동중인 로드밸런서를 선택하여 삭제를 진행한다.
(로드밸런서 삭제버튼 클릭)
다음으로
이렇게 하여 ELB 셋팅하기가 모두 마무리 되었다. 이번 글은 사실 EC2 배포, 탄력적 IP설정, Route53 도메인 연결, ELB 생성 및 EC2 대상 그룹설정, ACM을 통한 도메인 인증, ELB의 리디렉션 등등 많은 작업을 하나로 압축하여 진행하다보니 글이 길어졌다. 덕분에 진행하면서 많은 우여곡절이 있었고 도메인을 구매하고 호스팅영역을 절대 삭제해서는 안된다는 점도 알았다. 덕분에 todayvisible 주소를 이제 못쓸거같다.
다음으로는 데이터베이스 연결하기 (RDS)를 진행할 예정이다.
다음글 : [aws] rds(database) 생성하고 접속하기 (feat. mysql)
-
[diary] 리눅스 시험이 1주일 남았지만 당당히 공부 안하는 바쁜 사람.
intro : 5일만의 첫글이다. 나도 내가 이렇게 오랫동안 글 못쓸줄은 몰랐지.
1주일만인가? 이번주는 은근히 바쁜게 아니라 그냥 대놓고 바쁜 주였다. 블랙 프라이데이가 저번주 주말에 끝나기 직전. 인프런에서 다급하게 이전부터 눈여겨 보았던 강의를 구매하였다. 이전부터 devops로 도커랑 쿠버네티스를 공부해야지 생각만 했었는데 이번기회에 제대로 해보자 생각이 들어서 결단을 내렸다. 아무래도 최신 기술들의 적용을 위한 검색을 하면 도커 기반의 글들이 많다보니, 이제는 도커를 모르면 다른 사람의 블로그를 보고 코드를 따라하기도 어려운거 같다. 그래서 큰마음먹고 거금을 들여서 구매했고 이번주 4-5일 정도는 강의 듣는거에만 매진했다. (근데 아직도 다 듣지는 못했다.)
(내돈 내산, 도커와 쿠버네티스.)
(도커와 쿠버네티스를 공부한다면, CI/CD는 짝궁이지.)
사람들이 도커를 왜이렇게 많이쓰는가 했는데 들어보고 실습해보니까 도커정말 좋은기술이긴 하더라, 역시 사람들이 많이 쓰는것에는 다 이유가 있는것 같다. 이식성이 좋다는말이 백번말로 듣는거보다 한번내가 실습해보니 확 와닿는게 다르다. 도커 자체는 기술이 어려운거 같지 않은데, 이게 쿠버네티스로 scale-out 하는 과정에서 deployment랑 service volume 개념이 나오면서 aws에 이걸 얹으려고 하니까 어려워 지는거 같다. 나도 이번에 알았지만 aws에서 제공 안해주는 기술들이 없는거 같다. 뭐 별에별게 다있더만.
(devops도 열심히 하자. 아는게 힘인건 확실하다.)
aws 강의 아직 rds 부분 듣고있는데 한 20% 정도? 남은거 같다. 거의 마무리 단계인데 공부한 내용 블로그에 작성해야 하는데 언제 작성하나 제엔장. 지금 블로그에 포스팅해야하는 글들이 줄줄이 대기중이다 백준 문제 푼것도 안올린것도 5개 이상되고, aws도 들은거 정리해야하는데 그것도 한 3개분량 나올거같고, 도커는 진짜 거의 다들어서 이건 한1주일 도커 내용만 작성해야할거 같고, 쿠버네티스도… ci/cd도 정리할게 산더미다. 이번주에 좀 깊게 공부하기보다는 넓고 얕게라도 실무적으로 필수적으로 알아야하는 기술들 위주로 대량으로 공부를 해버린거 같다. 덕분에 블로그 신규 글 작성이 저번주가 5일전이다.
(하품 그만하고. 글 작성언제할건데 대체.)
블로그 글 작성할거 생각하면 그냥 입 찢어지게 하품만 나온다. 그나저나 공부도 체력이 받침이 되어야 가능한거 같다. 하고싶어도 힘들어서 못하는건 늙었다는 증거일까? 아니면 정신적으로 헤이한걸까? 어디쪽인지는 모르겠지만 이번주는 최선을 다하긴 했다… 나에게 스스로 칭찬해주고싶다. 나쁜어린이표 마냥 칭찬스티커 스스로 내 이마에 붙여주고 싶다. 잘했어.
그리고 이번에 블로그 카테고리를 대대적으로 개편을 했다. 앞으로 작성할 카테고리가 넓어짐이 예상됨에 따라 확장성과 유연성을 가지고 가기 위해서 그냥 세부적으로 각각 카테고리를 가지고 가지 않고 포괄적으로 가지고 가기로 방향성을 정했다. 덕분에 좀 깔끔해 진거 같기도 하고? 구분점이 오히려 없어진거 같기도하다. 모든 일에는 트레이드 오프가 있다고 했던가? 그 단어를 지금 이 상황에 쓰는게 맞는건지는 모르겠지만 단어가 멋있어서 한번 써본다.
카테고리를 정리하는김에 블로그에서 눈에 거슬리는 부분들도 개편을 했다. 예를든다면 카테고리앞에 </> 이 모양을 제대로된 svg 파일을 구해서 적용하여 글자를 확대해도 깨지지 않는다. 또 카테고리의 2단계 에서는 카테고리가 - 마이너스 모양으로 카테고리 명 앞에 붙었는데 일관성을 줄 수 있도록 </> 전부 적용했다. 별거 아닌거 같아도 이상하게 신경이 쓰이더라.
(별거 아닌거 아는데. 진짜 신경쓰였다고.)
그리고 나의 홈페이지의 I am visible에 마우스 커서를 올리면 집 모양이 나왔는데 내 시그니처 이미지를 만들어서 다음과 같이 눈 모양이 나온다. 귀엽지 않은가? 뭔가 보인다 라는 똣과도 연관성도 있어보이고 의미부여가 여러모로 되는 부분이다.
(눈 귀엽지?)
(다크 모드에서는 하얀색으로 눈 모양이 바뀐다.)
위 다크모드에 할말이 있는데 눈에 다크모드일때 하얀색으로 색 적용하는데 시간이 꽤 걸렸다. svg 파일에 어떻게 색을 부여하는지 몰라서 GPT랑도 씨름 여러번하고 오늘 아침에서야 방법을 찾아서 적용했다. 결론은 생각보다 심플했는데 filter: invert(1) brightness(10); 값을 적용하는것이다. 이게 css로 fill : white 뭐 어쩌구 하면 될거 같았는데 계속 적용이 안되어서 결국 우회하는 방법으로 직접적인 색상을 적용하는게 아니라, 검정의 반대로 색상을 반전시켜서 하얀색으로 변형하였다. 이것도 나름 해법이라면 해법인데 깨름칙하지 못한 해법이랄까. 아직도 css는 나에게 너무 어렵다.
눈 로고 이미지이야기는 여기서 마무리하고, 뜬금없지만 자랑할게 있다. 바로 내 포트폴리오 사이트에서의 로고 이미지를 어제 결정했다. 해리포터에서의 볼드모트가 자신의 이름을 지팡이로 나열할때의 모습이 오마주 된다. 나름 느낌있지 않은가?
(I am visible의 글자가 하나하나 뭔가 공중에 살아 숨쉬듯이 떠있는게 포인트다.)
(이사진 보면 뭔 느낌인지 알겠지?)
여자친구가 디자인해준건데 아주 마음에 든다. 생각보다 감각이 뛰어난거 같다. 대충해주는거 같은데 가끔 보면 전문적이랄까. 그래서 포트폴리오 사이트의 나머지 화면 디자인을 아직 건들이기 전이라서 해리포터 느낌으로 일관성있게 이것도 하나씩 바꿔나갈 생각이다. 벌써부터 재미가 있을거 같다. 디자인쪽이 품이 많이들기는 하는데 하고나면 재밌기는 한거 같다. 아무래도 바꾸고나면 이뻐지니까 자꾸 손을 대게 되는거 같다.
블로그 눈 로고 이미지 이야기 하다가 여기까지 왔는데 다시 돌아가자면, 눈 로고만 바꾼게 아니라, 이번에 toc 쪽도 건들였다. toc가 뭔가 했는데 table of control의 약자란다. 별걸 다 줄여서 부르는거 같다. 사파리 느낌으로 디자인을 바꿔줬다.
(화면 우측의 contents 부분이 toc이다.)
이전에는 그냥 떡하니 텍스트만 나오니까 별로인거 같았는데 다른 분들 블로그 보니까 toc 부분이 사라지고 다시 나타나고 자유자재더라. 나는 그정도는 못하더라도 좀 보기라도 좋게 해두기는 해야할거 같아서 디자인을 좀 변경했다. 나름 일관성 있게 사파리창으로 통일해주었다. 하고 나니까 굉장히 보기가 좋아지긴 한거 같다. 다크모드일때도 이쁘고 요즘 점점 블로그가 이뻐지고 있다.
(나름 블로그 유니크하고 이쁘지않은가? 나만 그래?)
마지막으로 블로그 자랑은 오늘 적용 완료한 기능만 자랑하면 끝일거 같은데. 이전부터 이미지 확대 기능이 없어서 조금 아쉽다는 생각을 했다. 모바일에서는 두손으로 확대를 해서 보면 되지만 네이버 블로그 같은거 보면 이미지를 클릭하면 이미지 뷰어에서 열리고 확대기능도 제공해주고 이미지만 따로 모아서 옆으로 넘겨서 보는 기능이 제공되는게 나도 그 기능을 내 블로그에 적용을 하고싶었다. 생각만 하다가 오늘 여러 라이브러리를 적용해보다가 그나마 괜찮은걸 찾아서 적용을 했다. 생각보다 적용도 간편하고 변경포인트도 적어서 아주 마음에 들었다.
fancybox라는 라이브러리인데 설정이 아주 간편하고 효과는 확실하다. 내 블로그의 모든 이미지는 이제 다 이렇게 보여지고 확대도 가능하다 후후.
(이제 내 블로그의 이미지는 클릭이 가능하고 해당 글의 이미지만 모아서 확인이 가능하다. 얼마나 편리한가?)
이미지 뷰어 기능도 제공하니까, 이제야 좀 블로그 같달까? 질좋은 글만 좀 채워두면 볼만한 블로그가 될거 같다는 생각이 들었다. 근데 글쓰는게 여간 힘든게 아닌거 같다. 글쓰다가 하루가 다간다 정말로.
블로그 자랑은 여기까지이고, 앞으로 고쳐야 할 기능인 검색 부분만 손보면 크게 고칠건 다 고친거 같다. 검색 기능이 좀 마음에 안든다. 기본적으로 제공되는 기능이 왜 다 뭔가 부족한 기능들만 있는가. 커스텀을 안할래야 안할수가 없다.
(꼴보기 싫은놈이 아직 하나 남았다.)
갑자기 이야기의 급 전개로 뜬금없지만, 내년에는 부트캠프를 하고자 한다. 협업을 통한 프로젝트 구성을 해본 경험도 필요하고 나름 실력적으로 성장을 하고싶은데, 가장 빠르고 좋은 방법이 부트캠프인거 같다. 지금 노리는 부트캠프는 LG에서 시행하는 부트캠프인데 내일 본가로 돌아가면 신청할 생각이다. 모집정원이 30명이라 될지모르겠지만 되면 좋고 안되면 말고 마음 편히먹고있다. 어차피 어디서 부트캠프를 하던지 내가 열심히 하냐마냐가 가장 중요한게 아닌가?
(그래도 여기서 하면 좋으니까. 제발 뽑아주세요. 잘할게요.)
흠흠. 되면 좋지 안되면 말고 뭐… 그래도 도전은 해보자는 주의다. 내일 자기소개서를 작성해야하는데 내일 불티나게 타자기를 두드리고 있을거 같다. 그나저나 내 이력서에 첨부할 사진이 어딨더라? 한참 찾고있는데 이전 회사 다닐때 찍어둔 사진들은 다 어디로 간건가 싶다. 분명히 여러장 가지고 있었는데. 내일 뭔가 쓸데없는거에 시간 소비하느라 바쁠거같다.
그나저나 요즘 무언가에 집중하는 시간이 점점 짧아지는거 같은데 남는 시간에 운동이라도 하려고 기구를 샀다. 물론 내돈주고 산건 아니고 여자친구가 사줬는데 기록용으로 사진하나 남긴다. 앞으로 진심으로 몸짱이 되고 싶다고 생각이 들어서 운동을 시작했다. 시간을 정해두고 운동을 하는 스타일은 아닌데 그래도 체력적으로 관리해야 하는 시점이 온거 같아서 최대한 나름의 관리를 하려고 틈틈이 노력중이다.
(윗몸일으키기 하는 기구이다.)
근데 웃긴건 여기 브랜드가 약간 좀 잘못읽으면 이거쥐~~~~ 인거 같다. 나만 그렇게 읽어지는가?
(너무 억지야?)
긍정마인드라 그런가 저런것만봐도 웃음이 나온다. 인생 뭐있나 행복하게 사는게 장땡이지. 🙃
아. 제목에 시험 1주일 남았는데 당당히 공부안하는 바쁜사람이라고 적어놨는데 위 내용만 읽어봐도 얼마나 눈코뜰새없는지 알지않겠는가? 그래도 시험 비용이 5만원이나 주었기 때문에 꼭 이번에 취득할거다. 그러고보니, 또 이러면 다음주 블로그 글 작성이 없는건 아닌가 모르겠다. 시험 준비한답시고 다른 java나 jpa를 공부할수 있는 시간이 과연…날것인지…는 의문이다. 그래도 시험주에는 시험만 집중하는게 낫겟지? 그래도 마음편하게 준비하려고 한다. 못보면 어때 다음에도 도전하면 되지. faker선생님이 실패는 작은 성공이라고 했어. 암요 그렇구 말구요.
다음 근황글을 작성할때는 당당하게 시험 합격에 대해 이야기를 할 수 있도록 한주를 잘 보내 봐야겠다. 오늘은?
(거기까지.)
-
[aws] route53을 통해 도메인 구매하고 연결하기 (feat. ec2)
intro : route53을 통해 도메인을 구매하고 연결하는 방법에 대해서 알아보자.
이전 글 [aws] ec2를 통해 백엔드 api 서버 배포하기 (2) 과 이어집니다.
이 글을 읽기전 주의사항
이 글을 보고 도메인을 구매 한 후 호스팅 영역에서의 레코드를 절대 함부로 삭제하지 마세요.
Route53 이란?
간단하게 이야기해서 도메인을 발급하고 관리해주는 서비스이다. 조금 더 전문적인 용어로 표현하면 DNS(Domain Name System)이라고 한다.
도메인 이란?
www.naver.com, www.google.com 등 문자로 표현된 인터넷 주소를 뜻한다.
DNS(Domain Name System)란?
도메인이 없던 시절에는 특정 컴퓨터와 통신하기 위해서 IP주소를 사용했다. 이 IP는 특정 컴퓨터를 가르키는 주소의 역할을 한다. 하지만 IP 주소는 많은 숫자들로 이루어져 있어서 일일이 외우기 너무 불편했다. 이를 해결하기 위해 IP 주소를 문자로 변환해 주는 하나의 시스템(서버)를 만들게 되었다. 이게 바로 DNS이다.
도메인 기반으로 통신하는 이유
일반적으로 서버는 IP기반으로 통신하지 않고 도메인기반으로 통신한다. 이유는 여러가지지만, 많은 이유 중 하나는 HTTPS의 적용 때문이다. IP주소에는 HTTPS적용을 할 수 없다. 도메인 주소가 있어야 HTTPS 적용을 할 수 있다. 이 때문에 특정 서비스를 운영할때 도메인은 필수적으로 사용하게 된다.
ec2 인스턴에 도메인 연결하기
Step1. Route53에 연결할 EC2 생성하기
이 단계부터는 하기 글들의 선행이 필요합니다.
[aws] ec2를 통해 백엔드 api 서버 배포하기 (1)
[aws] ec2를 통해 백엔드 api 서버 배포하기 (2)
Step2. Route53에서 도메인 구매
가장 먼저 route53을 이용하여 도메인을 구입해 볼건데, ec2 항목을 찾아들어가듯이 검색창에 route53을 검색하여 대시보드로 이동하자.
(ec2처럼 route53도 대시보드가 존재한다.)
대시보드에서 도메인 등록버튼이 보이는데 버튼을 클릭하면 다음과 같은 화면이 나온다.
(여기서 내가 원하는 도메인을 찾아보고 구매할 수 있다.)
그럼 내가 원하는 도메인을 검색하고 선택하면 되는데, 다음과 같이 원하는 도메인을 선택한다.
(구입하고자 하는 도메인 선택 (이미지는 예시입니다. 본인이 원하는 도메인을 선택해주세요.))
결제 진행 버튼을 누르면 다음과 같은 화면이 나오는데 요금 옵션은 1년부터 다년까지 여러가지 옵션으로 구매할수 있다. 나는 1년으로 결재하고 자동갱신을 클릭해서 1년이 지나면 자동으로 구매가 될 수 있도록 설정하였다.
(자동갱신까지 선택)
결재인원의 개인정보를 입력해야 하는데, 다른 부분은 간단히 작성하면 되고, 이메일 부분은 특히 신경써서 작성해야한다. 앞으로 결재내역이 발송될 이메일 이기에 잘 작성해두자.
(이메일 부분은 신중하게 작성하자.)
개인정보까지 입력을 잘 했으면 마지막 확인 페이지가 나온다. 작성한 내용이 문제가 없다면 제출버튼을 클릭해 도메인 구입을 완료하자.
(도메인 구입 완료!)
내가 구입한 도메인을 확인하기 위해서는 Route53 > 호스팅 영역 으로 이동하면 되는데, 도메인 구입후에 어느정도 시간이 지나야만 이 페이지에서 내가 구입한 도메인을 확인할 수 있다. 본인은 10분정도 소요가 되었던 것 같다. 또한 도메인 > 등록된 도메인 에서도 내가 구입한 도메인을 확인할 수 있다.
(내가 구입한 도메인 확인하기 (1))
(내가 구입한 도메인 확인하기 (2))
Step3. Rout53에서 구매한 도메인 EC2 퍼블릭 IP에 연결하기
구매한 도메인을 이제 IP에 연결해보자. 다음과 같이 호스팅 페이지로 이동한다. 그 후 내가 구입한 도메인을 클릭하자.
(도메인이름을 클릭하자, 파란색으로 링크가 보인다.)
그럼 다음과 같이 화면이 보이는데, 여기서 레코드 생성을 눌러준다.
(레코드 생성을 누르자.)
레코드 생성페이지에서 본인이 연결하고자 하는 퍼블릭 ipv4 값을 정확히 입력한다. 레코드 유형은 A 그대로 선택한다. 그 뒤 레코드 생성을 누르자.
(레코드 유형은 기본값인 A를 선택하고, ipv4값을 정확히 입력하자. 레코드이름에 www를 붙여주면, 내 도메인 앞에 www.todayvisible.com 으로 접속할 수 있다. 입력을 하지 않으면 todayvisible.com 으로 접속이된다.)
위 과정을 완료하였다면 브라우저에 내가 구입한 도메인 주소를 입력해보자. 이미지와같이 내가 생성한 ec2에 접근할 수 있다.
(내가 구입한 도메인주소로, 이전에 생성한 api-server ec2에 접근 성공!)
다음으로
이렇게 하여 ec2를 통해 백엔드 api 배포한 서버를, 구입한 도메인으로 연결 및 접속하기가 마무리 되었다. 다음으로는 도메인에 HTTPS를 어떻게 적용하는지 학습할 예정이다.
다음 글 : [aws] elb(load balance)에 https 연결하기 (feat. ec2)
-
[aws] ec2를 통해 백엔드 api 서버 배포하기 (2)
intro : 백엔드 api 서버를 배포하는 방법에 대해서 알아보자.
이전 글 [aws] ec2를 통해 백엔드 api 서버 배포하기 (1) 과 이어집니다.
EC2 설정하기
이전글에서 인스턴스를 생성하였고, 이번글에서는 EC2가 생성되었다는 가정하에 이어서 글을 작성한다.
Step4. 생성된 EC2 살펴보기
만약 이전글을 보고 EC2 인스턴스를 정상적으로 생성하였다면 다음과 같이 EC2 인스턴스 항목에 아래의 이미지를 확인 할 수 있다.
(시간이 조금 걸릴수도 있다. 보통 5분 내외로 이렇게 생성된 인스턴스 목록이 보인다.)
인스턴스 아이디 항목이 파란색으로 링크가 있는데 그걸 클릭하면 다음과 같은 화면이 보여진다.
(가장 중요한 정보는 퍼블릭 ipv4와, 인스턴스 상태이다.)
퍼블릭 ipv4란?
퍼블릭 ipv4는 EC2 인스턴스가 생성되면서 부여받은 IP 주소이다. 인스턴스에 접근하려면 이 IP 주소로 접근하면 된다.
인스턴스 상태란?
인스턴스 상태는 말그대로 EC2가 어떠한 상태인지를 나타내는데, 만약 실행중 이라고 떠 있다면, 컴퓨터가 정상적으로 실행중임을 뜻한다.
Step5. EC2 접속하기
이제 본격적으로 EC2에 접속할건데, 최상단 우측의 빨간색 박스로 연결 버튼이 있는데 클릭을 해보자.
(연결 버튼을 클릭하자.)
그럼 아래와 같은 이미지가 보이는데, 기본적으로 선택되어 있는 항목은 변경하지 말고, 연결 버튼만 클릭한다. (사실 SSH를 통해 연결하는 방법을 주로 사용하기는 하지만, 기본적인 EC2 인스턴스 연결 방법을 통해 먼저 접속해 보겠다.)
(연결 버튼을 클릭하자.)
그럼 다음과 같이 새창이 생성 되면서 리눅스 화면을 AWS 콘솔 웹에서 확인할 수 있다. 기본적인 명령어 pwd 입력하여 출력결과를 확인해보자.
(셍성된 인스턴스에 pwd 명령어를 입력해보자.)
Step6. 탄력적 IP 연결하기
탄력적 IP가 왜 필요할까?
우리가 위 단계를 통해 인스턴스를 생성하여 할당받은 IP는 임시 IP이다. EC2 인스턴스를 중지시켰다가 다시 실행시키면 IP가 변경되어 있다. 그래서 중지시켰다가 다시 재 실행하여도 변경되지 않는 고정 IP가 필요하다. 이게 바로 탄력적 IP 이다. 인스턴스를 생성하고 나서 필수적으로 설정해야하는 탄력적 IP를 진행해 보도록 하겠다.
(네트워크 및 보안의 탄력적 IP를 클릭하자)
그럼 아래와 같은 페이지로 이동하는데 우측 상단의 주황색 버튼 탄력적 IP 할당을 클릭한다.
(탄력적 IP 할당을 클릭하자.)
이후 페이지에서도 기본값을 변경하지 않고, 할당 버튼을 클릭하자.
(할당 버튼을 클릭하자)
탄력적 IP 주소가 정상적으로 할당된 모습이다.
(탄력적 IP 주소 할당 성공!)
할당된 탄력적 IP 주소를 우리가 만든 인스턴스에 연결을 해볼건데 다음과 같이 탄력적 IP 주소 연결 버튼을 클릭한다.
(인스턴에 탄력적 IP 주소를 연결해보자.)
이때 인스턴스 연결항목에서 내가 탄력적 IP주소와 연결하고자 하는 인스턴스를 반드시 선택하고 연결 버튼을 클릭한다.
(연결하고자 하는 인스턴스를 반드시 선택해야한다.)
이제 스프링부트를 배포하기까지 모든 준비가 끝났다. 준비된 환경에서 스프링부트를 우리가 만든 인스턴스에 배포하고 접속해보자.
백엔드 api-server EC2 인스턴스에 배포하기
Step1. jdk 17 install
가장 먼저 다음 명령어를 우리가 생성한 EC2 ubuntu에 입력하여 jdk를 설치한다. jdk를 17 버전으로 설치하는 이유는 우리가 배포할 스프링부트의 버전이 3.0 이상의 버전이기 때문이다. 다음 프로젝트를 예시로 배포하길 추천한다. (본인이 만든 프로젝트를 배포해 봐도 됩니다.)
// 1번 명령어
sudo apt update
// 2번 명령어
sudo apt install openjdk-17-jdk -y
다음 명령어로 jdk가 정상 설치되었는지 확인한다.
// 자바 버전이 아래와 같이 출력된다.
java --version
// openjdk 17.0.13 2024-10-15
// OpenJDK Runtime Environment (build 17.0.13+11-Ubuntu-2ubuntu124.04)
// OpenJDK 64-Bit Server VM (build 17.0.13+11-Ubuntu-2ubuntu124.04, mixed mode, sharing)
Step2. sample 프로젝트 git clone
jdk 설치가 완료되었다면, git 명령어를 통해 프로젝트를 설치해보자.
// EC2 배포 테스트를 위한 sample 프로젝트이다.
git clone https://github.com/sbi1024/api-server-ec2.git
Step3. sample 프로젝트 build 하기
git 명령어로 다운받은 프로젝트를 api 폴더로 이동하여 build를 진행한다.
// api-server-ec2 폴더로 이동
cd api-server-ec2
// 혹시 빌드된 파일이 있다면 지우고, 빌드한다.
./gradlew clean build
// 만약 권한문제가 발생한다면 다음과 같은 명령어를 실행한다.
chmod +x gradlew
(다음과 같이 화면에서 실행된다. 시간이 조금 걸리는거 같다.)
Step4. sample 프로젝트 build 파일 실행하기
먼저 build된 파일이 존재하는 위치로 이동해야 하는데, 다음과 같은 위치에 build 된 파일이 존재한다. ~/api/build/libs/ 아래 이미지에서도 확인 할 수 있고, 정상적으로 폴더를 이동하였으면 build된 2개의 파일이 존재하는데 SNOPSHOT.jar 파일을 실행한다. (plain은 안정화된 단계에서 주로 사용하고, snapshot은 개발 및 테스트 단계에서 사용한다.)
// build 된 파일이 존재하는 폴더로 이동
cd ~/api-server-ec2/build/libs
// 빌드된 SNAPSHOT.jar 파일 실행
sudo java -jar api-server-ec2-0.0.1-SNAPSHOT.jar
(java -jar 명령어를 통해 실행하려면 폴더를 이동해야한다.)
위 명령어를 통해 정상적으로 실행하였다면 다음과 같이 출력된다. 인텔리제이에서 백엔드 서버를 개발하였을때 자주보던 출력문구들이다.
(80 포트로 잘 실행된 것을 알 수 있다.)
80 포트로 실행된 것을 확인하였으면, 본인의 ipv4 주소로 브라우저에 입력하여 접속해보자. 80포트이기때문에 ip주소값만 브라우저에 입력하여도 정상적으로 접근이 된다.
(이미지의 문구를 정확히 확인하였다면 정상적으로 배포가 되었음을 알 수 있다.)
EC2 종료하기
지금까지 EC2 인스턴스를 설정하고 생성하여 스프링부트를 배포하고 접속까지 해보았다. 이제 우리가 만든 인스턴스를 종료하는 방법을 알아보자.
EC2 인스턴스 종료하기
인스턴스를 종료하는건 생성하는거보다 간단하다. EC2 인스턴스 항목에서 인스턴스 상태를 인스턴스 종료(삭제) 를 클릭한다.
(삭제 버튼 클릭)
여기서도 종료(삭제) 버튼을 클릭한다.
(종료(삭제) 버튼 클릭)
인스턴스는 위 단계로 종료(삭제)가 되지만, 아직 탄력적 IP주소를 릴리즈 하지 않았다. 아래의 단계를 거쳐서 탄력적 IP 주소도 릴리즈 하자.
EC2에 연결하였었던 탄력적 IP 주소 릴리즈
작업버튼을 클릭하고, 탄력적 IP 주소 릴리즈 버튼 클릭하자
(탄력적 IP 주소 릴리즈 버튼 클릭!)
묻지도 따지지도 말고 릴리즈 버튼을 클릭하자.
(릴리즈 버튼 클릭!)
위 두개의 단계를 거치면 AWS를 통해 실행하고 있는 그 어떤것도 없기 때문에 비용이 나가지 않게된다. (프리티어라고 해서 무조건적으로 무료로 제공해주는건 아니다. 제한사항이 존재한다.)
다음으로
이렇게 하여 ec2를 통해 백엔드 api 서버 배포하기가 마무리 되었다. 다음으로는 도메인 연결하기 (Route53)에 대해서 학습할 예정이다.
다음 글 : [aws] ec2 인스턴스에 도메인(route53) 구매해서 연결하기 (feat. ec2)
-
-
[aws] ec2를 통해 백엔드 api 서버 배포하기 (1)
intro : 백엔드 api 서버를 배포하는 방법에 대해서 알아보자.
배포란 무엇인가 ?
배포란, 다른 사용자들이 인터넷을 통하여 우리가 만든 서비스를 이용할수 있게 하는 것을 말한다. 쉽게 이야기하면 내가 만든 웹 사이트나 서버를 다른 사용자가 사용하려면 인터넷 상에 배포가 되어 있어야 한다. 배포를 하게되면 localhost가 아닌, IP(124.87.9.8)나 도메인(www.naver.com)과 같이 고유한 주소를 받게 되고, 다른 컴퓨터에서 그 주소로 접속할수 있게 된다. 이게 바로 배포이다.
EC2란?
EC2는 Elastic Compute Cloud의 약자로 C가 2번 들어가서 EC2로 명명되었다. 컴퓨터를 빌려서 원격으로 접속해 사용하는 서비스 이다. 쉽게 이야기하면 하나의 컴퓨터를 의미한다.
EC2를 왜 사용해야 할까?
서버를 배포하기 위해서는 컴퓨터가 필요하다. 내 노트북을 통해서도 배포할 수 있다. 다만 24시간 내내 컴퓨터를 켜 두어야 하고, 전기세도 많이나갈것이다. 또한 다른 사용자들이 내 컴퓨터에 접속할수 있어야하니 보안적으로도 굉장히 취약할 것이다. 그렇기에 이러한 불편한 점을 해소시켜주는 기능이 제공되는 AWS EC2를 사용하는 것이다. 이외에도 오토스케일, 로드밸런싱 등 다양한 부가 기능을 제공하기도 한다.
프론트 서버도 EC2에 배포하나요?
프론트 서버도 EC2에 배포할 수 있다. 다만 vercel, netlify, AWS S3를 주로 사용한다. EC2는 백엔드 서버를 배포하는데 주로 사용 된다.
EC2 설정하기
가장먼저 다음과 AWS 링크를 이동하여 회원가입을 진행하고, 로그인을 진행한다. 로그인 진행 후 접속하면 다음과 같은 화면을 마주하게 된다.
(최상단 우측의 리전(Region)을 선택하자.)
Step1. EC2 리전(Region) 선택하기
리전(Region) 이란 ?
인프라를 지리적으로 나누어 배포한 각각의 데이터 센터를 말한다. 좀 더 쉽게 설명하면 내가 컴퓨터를 빌려서 원격으로 접속할 컴퓨터의 위치 즉 지역을 의미한다.
리전(Region)의 특징
AWS는 전세계적으로 다양한 Region을 보유하고 있다. 각 Resion은 고유한 이름이 있다. (버지니아 북부는 us-east-1, 오하이오는 us-east-2 등 고유한 이름을 보유한다.)
리전(Region)을 선택하는 기준
사람들이 애플리케이션을 사용할 때는 네트워크를 통해 통신하게 된다. 이 때 사용자의 위치와 애플리케이션을 실행시키고 있는 컴퓨터의 위치가 멀면 멀수록 속도가 느려진다. 따라서 애플리케이션의 주된 사용자들의 위치와 지리적으로 가까운 리전을 선택하는게 유리하다.
다음과 같이 리전(Region)을 아시아 태평양(서울) 으로 선택하자. 다른나라로 선택하면 안된다. 그 이유는 아래에서 자세하게 설명한다.
(최상단 우측의 리전(Region)을 선택하자.)
많이 하는 실수 (주의사항)
AWS는 리전마다 EC2가 각각 관리되고 있다, 즉 서울에서 EC2를 만들었는데 리전을 버지니아 북부로 변경하면 나의 EC2가 보이지 않는다는 말이다. 이럴때는 당황하지 말고 AWS 콘솔창에서의 리전 설정값을 잘 확인해보자.
(하나의 Region에 고유한 이름이 여러개 있다.)
Step2. EC2 셋팅하기 - 기본 설정
AWS 콘솔창의 검색창에서 EC2 로 검색한다. 아래 이미지와 같이 검색 후 클릭하여 이동한다.
(최상단 좌측에 검색하는 창이 있다.)
그 이후 다음과 같은 화면이 나오는데 여기서 인스턴스 시작 버튼을 클릭한다.
(인스턴스 시작 주황색 버튼을 클릭한다.)
이름 및 태그
그러면 아래와 같이 화면이 나오는데, 가장 먼저 해야할 부분은 이름 및 태그 부분에 EC2 인스턴트 끼리 식별할수 있는 이름을 작성한다. 이름을 신중히 작성하는게 좋은데, 그 이유는 인스턴스는 결국 컴퓨터이고 해당 컴퓨터가 어떤 역할을 하는지 이름으로 식별할 수 있기 때문이다. 만약 개발용으로 사용할 서버와, 실제 배포용으로 사용할 서버가 이름이 같다면 구별하기 힘들지 않겠는가? 그러니 나의 상황에 맞게 이름을 작성한다. 나 같은 경우는 테스트용으로 단순히 api-server 라고 지정하였다.
(이름은 내가 식별할 수 있게만 작성하면 된다.)
애플리케이션 및 OS 이미지(Amazon Machine Image)
다음으로는 애플리케이션 및 OS 이미지를 선택한다. Mac os와 Window os가 익숙하겠지만, 서버용으로 사용할 OS는 리눅스가 최적화 되어있다. 그 이유로는 Mac, Window 같은 os는 컴퓨터를 쉽게 사용할 수 있도록 부가적인 기능을 제공하기에 리눅스에 비해 용량도 많이 차지하고 성능적으로 떨어진다. 그렇기에 서버를 배포할때 필요한 기능만 포함되어 있는 os를 선택하는게 유리하기에 Ubuntu를 쓴다.
(서버용으로 사용할 OS는 리눅스를 보통 사용한다.)
인스턴스 유형
다음으로 인스턴스 유형을 선택해야 한다. 인스턴스 유형이 뭔지 알면 해당 값을 설정하는데 있어서 아주 큰 도움이 되는데, 쉽게 설명하면 내가 빌릴 인스턴스 즉 컴퓨터의 사양을 뜻한다. 즉 좋은 사양의 인스턴스를 빌릴지, 좀 사양적으로 부족한 인스턴스를 빌릴지 이 Step에서 선택하는 것이라고 볼 수 있다. 다만 나는 t2.micro 를 선택하여 프리티어 즉 요금이 부과되지 않는 유형을 선택하였다. 또한 이 유형은 하루 평균 2000명 정도 사용되는 사이트에서도 충분히 운영 가능함이 보장된 유형이기에 첫 서버 배포를 할때 사양을 낮은 단계에서 운용하고, 차후에 상황에 맞에 유형을 높은 버전으로 변경하는 것을 추천한다.
(첫 시작은 t2.micro로 시작해도 충분하다.)
키 페어(로그인)
다음으로 키페어(로그인) 부분에서 새 키 페어 생성을 눌러준다. 그럼 아래와 같은 이미지처럼 화면이 나오는데, 키 페어 이름을 설정하고 키 페어 생성 버튼을 눌러주면 파일이 다운로드 된다. 다운로드된 파일은 내가 생성한 인스턴스 EC2에 접속할때 필요한 파일이며, 해당 파일을 통해서 SSH 로그인이 가능하다.
(이름 및 태그에서처럼 내가 식별할 수 있도록 이름을 지정한다.)
네트워크 설정
다음으로 아래의 네트워크 설정에서 편집 버튼을 클릭한다. 편집 창에서 방화벽(보안그룹)항목이 중요한 부분이니 하기에서 먼저 개념에 대해서 이해하고 편집창에서 어떤식으로 값을 설정하는지 알아보도록 하겠다.
(우측 상단의 편집 버튼 클릭)
하기 이미지는 편집 버튼을 클릭하면 바뀌는 화면이다.
(편집 버튼 클릭하면 열리는 창)
방화벽(보안그룹) 설정 전 보안그룹의 개념에 대해서 알아보자.
보안그룹이란, AWS 클라우드에서의 네트워크 보안을 의미한다. EC2 인스턴스를 집이라고 생각한다면, 보안 그룹은 집 밖에 쳐져있는 울타리와 대문같은 개념이다. 인터넷에서 일부 사용자가 우리가만든 EC2 인스턴스를 접근한다고 가정해보자. 인스턴스에 방화벽 역할을 할 보안 그룹을 만들고 규칙을 설정하는데 이때 인바운드 규칙(외부에서 EC2로 보내는 트래픽)과 아웃바운드 규칙(EC2에서 외부로 보내는 트래픽)을 설정하여, 특정 사용자만 접근할 수 있도록 설정할 수 있다. 이때 만약 내가 인바운드 규칙을 내 PC 아이피에서만 접근할 수 있도록 설정한다면, 일부 사용자는 우리가 만든 EC2 인스턴스에 접근할 수 없다.
보안 그룹을 설정할때는 허용할 아이피(IP)와 포트(PORT)를 설정할 수 있다.
이어서 보안그룹을 설정하는 화면을 다시 보자면, 먼저 보안 그룹 이름을 설정한다, 나는 api-server-security-group 이라고 명명하였다. 아래 이미지를 보면 기본적으로 ssh 접속이 허용되어 있는데, 어느곳에서든 22번 포트로 해당 인스턴스를 접근하는것에 대해 열려있다는 것을 뜻한다. 왜냐하면 SSH를 통해 내가 어디서든지 원격 접속을 하기 위해 허용되어야 한다고 볼 수 있다. 또한 이 server는 불특정 다수에게 접근을 허용해야 하기에, 보안 그룹 규칙 추가 버튼을 클릭하여 HTTP로 접근하고자 하는 사용자들에게 접근을 허용할 수 있도록 추가 해주어 설정을 마무리 한다.
(SSH 22포트, HTTP 80포트를 인바운드 규칙으로 허용해줌)
IP의 개념
IP는 특정 컴퓨터의 주소를 뜻한다.
PORT의 개념
한 컴퓨터 내에 실행되고 있는 프로그램을 뜻한다.
스토리지 구성
EBS는 Elastic Block Storage의 약자이다. 인스턴스도 하나의 컴퓨터이다 보니 파일을 저장할 공간이 필요한데, 이 저장공간을 EBS라고 부른다. 즉 EBS란 EC2안에 부착된 하드디스크 라고 생각하면 된다. EBS 보다 더 포괄적인 용어로는 스토리지, 볼륨 이라고 부른다. 기본적으로 아래 이미지의 GB 부분이 8GB로 설정되어 있는데 30GB로 변경해주자. 프리티어는 30GB까지 허용해준다, 또한 범용 SSD GP3로 변경한다.
(프리티어는 30GB 까지 허용해준다.)
Step3. EC2 인스턴스 생성
이후 우측의 인스턴스 시작 버튼을 클릭하면. 기본설정에 기반한 인스턴스가 생성되고 AWS 콘솔창에서 내가 생성한 인스턴스를 확인 할 수 있다.
(주황색 인스턴스 시작 버튼을 클릭하자.)
다음으로
다음 글에서는 생성된 EC2에 접속하고 EC2에 다양한 부가적인 설정을 진행하며, 스프링부트를 EC2 환경에 배포하는걸 해보도록 하겠다. (비용나가지 않게 하는 방법도 포함)
다음글 : [aws] ec2를 통해 백엔드 api 서버 배포하기 (2)
-
[programmers] 머쓱이보다 키 큰 사람 (프로그래머스 java 풀이)
intro : level0 문제라 굉장히 쉽게 풀 수 있다.
프로그래머스 문제링크
문제 설명
머쓱이는 학교에서 키 순으로 줄을 설 때 몇 번째로 서야 하는지 궁금해졌습니다. 머쓱이네 반 친구들의 키가 담긴 정수 배열 array와 머쓱이의 키 height가 매개변수로 주어질 때, 머쓱이보다 키 큰 사람 수를 return 하도록 solution 함수를 완성해보세요.
제한사항
1 ≤ array의 길이 ≤ 100
1 ≤ height ≤ 200
1 ≤ array의 원소 ≤ 200
입출력 예
array
height
result
[149, 180, 192, 170]
167
3
[180, 120, 140]
190
0
입출력 예 설명
입출력 예 #1
149, 180, 192, 170 중 머쓱이보다 키가 큰 사람은 180, 192, 170으로 세 명입니다.
입출력 예 #2
180, 120, 140 중 190보다 큰 수는 없으므로 0명입니다.
문제풀이
class Solution {
public int solution(int[] array, int height) {
// 리턴할 변수 선언
int answer = 0;
// 향상된 for문 실행 (for-each)
for (int i : array) {
// 매개변수로 주어진 height 값보다 큰지 판별
if (i > height) {
// 만약 크다면 +1
answer += 1;
}
}
// 결과값 반환
return answer;
}
}
// 테스트 1 통과 (0.02ms, 81.3MB)
// 테스트 2 통과 (0.01ms, 75.5MB)
// 테스트 3 통과 (0.02ms, 76.5MB)
// 테스트 4 통과 (0.01ms, 72.6MB)
문제 해석
주어진 height 값보다 큰 값을 array 배열안에서 찾아서 개수를 세어 반환하면 되는 문제이다. 단순히 배열을 반복문을 통해 순회하면서 height 보다 큰 값을 if 조건문을 통해 찾으면 된다.
-
[baekjoon] 피보나치 수 5 (백준 10870 java 풀이)
intro : 재귀함수는 싫다. 싫어.
백준 문제링크
문제
피보나치 수는 0과 1로 시작한다. 0번째 피보나치 수는 0이고, 1번째 피보나치 수는 1이다. 그 다음 2번째 부터는 바로 앞 두 피보나치 수의 합이 된다. 이를 식으로 써보면 Fn = Fn-1 + Fn-2 (n ≥ 2)가 된다. n=17일때 까지 피보나치 수를 써보면 다음과 같다. 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597 n이 주어졌을 때, n번째 피보나치 수를 구하는 프로그램을 작성하시오.
입력
첫째 줄에 n이 주어진다. n은 20보다 작거나 같은 자연수 또는 0이다.
출력
첫째 줄에 n번째 피보나치 수를 출력한다.
문제 풀이 (100ms)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// 입력값을 받아서 정수로 변환
int n = Integer.parseInt(br.readLine());
// 재귀함수를 호출하고 return 받음
int result = recursion(n);
// 결과값 출력
System.out.println(result);
// 자원 반납
br.close();
}
// 피보나치 계산을 수행할 재귀함수
public static int recursion(int n) {
// 0과 1은 각자 자기자신의 값을 반환함
if (n <= 1) {
return n;
}
// recursion(10) = recursion(9) + recursion(8) ... 1까지 반복됨
return recursion(n - 1) + recursion(n - 2);
}
}
문제 해석
재귀함수의 기초를 닦을수 있는 문제인거 같다. 난 재귀함수 문제는 반복문으로도 풀수 있어야 한다고 생각해서 아래와같이 재귀함수로도 풀어보고 반복문으로도 풀어봤다. 두개의 풀이방식에는 차이가 있지만 결국에 구하고자하는 결과값은 피보나치 수열의 값을 어떻게 계산할 것인가? 가 포인트가 되는거 같다. 피보나치 수열에서의 중요한 점은 다음과 같은 단계로 나누어 볼 수 있다.
// 각 단계에서 메서드가 종료된게 아님,
// return 문에서 새로운 recursion을 호출할때마다 해당 메서드 결과값 반환을 기다리고 있음
recursion(10) 호출 → recursion(9) + recursion(8) 계산을 기다림
recursion(9) 호출 → recursion(8) + recursion(7) 계산을 기다림
recursion(8) 호출 → recursion(7) + recursion(6) 계산을 기다림
recursion(7) 호출 → recursion(6) + recursion(5) 계산을 기다림
recursion(6) 호출 → recursion(5) + recursion(4) 계산을 기다림
recursion(5) 호출 → recursion(4) + recursion(3) 계산을 기다림
recursion(4) 호출 → recursion(3) + recursion(2) 계산을 기다림
recursion(3) 호출 → recursion(2) + recursion(1) 계산을 기다림
recursion(2) 호출 → 반환: 1 + 0 = 1
recursion(3) 반환 → 1 (recursion(2)) + 1 (recursion(1)) = 2
recursion(4) 반환 → 2 (recursion(3)) + 1 (recursion(2)) = 3
recursion(5) 반환 → 3 (recursion(4)) + 2 (recursion(3)) = 5
recursion(6) 반환 → 5 (recursion(5)) + 3 (recursion(4)) = 8
recursion(7) 반환 → 8 (recursion(6)) + 5 (recursion(5)) = 13
recursion(8) 반환 → 13 (recursion(7)) + 8 (recursion(6)) = 21
recursion(9) 반환 → 21 (recursion(8)) + 13 (recursion(7)) = 34
recursion(10) 반환 → 34 (recursion(9)) + 21 (recursion(8)) = 55
// 각 단계에서 값을 반환하고 차례대로 계산 진행
최종적으로 recursion(10)이 55을 반환
위 단계를 천천히 읽어보면 재귀함수의 호출 시점과 종료 시점에 대해서 알 수 있다.
반복문을 통해 재귀함수 없이 풀이 방법 - 106ms 소요
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter, StringBuilder 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
// 입력값을 받아서 정수로 변환
int input = Integer.parseInt(br.readLine());
// 배열의 크기를 21로 초기화
int[] array = new int[21];
// 인덱스 0번과 1번은 0과 1로 초기화
array[0] = 0;
array[1] = 1;
// 반복문을 통해 피보나치 수열의 각각의 값을 계산
for (int i = 2; i < array.length; i++) {
array[i] = array[i - 1] + array[i - 2];
}
// 입력값으로 받은 찾고자 하는 값에 해당하는 값 append()
sb.append(array[input]);
// bw로 출력하기 위해 문자열로 변환 후 write
bw.write(sb.toString());
bw.flush();
// 자원 반납
bw.close();
br.close();
}
}
마무리
호출 단계와 메서드 종료 시점을 확인해보니 각 해당 단계에서 중복된 계산 결과가 반복 되는것을 확인 할 수 있었다. (recursion(8)이 recursion(10)와 recursion(9) 에서 각각 한번씩 총 두번 호출 됨) 재귀함수에서는 각 계층에서의 호출이 각각 일어나기에 추적하기 어려웠던 부분인데, 그림으로 그려보고 눈으로 확인해보니 확실하게 알 수 있었던 것 같다. 다음에는 좀 더 최적화된 로직 구현 방법을 적용해 보고 싶다.
-
[design-pattern] singleton 패턴
intro : singleton 패턴에 대해서 알아보자
singleton 패턴이란 ?
싱글톤 패턴(Singleton Pattern)은 객체지향 프로그래밍에서 사용되는 생성 디자인 패턴 중 하나로, 클래스의 인스턴스가 오직 하나만 존재하도록 보장하는 패턴입니다. 이 패턴은 클래스가 단 하나의 인스턴스를 공유해야 할 때 유용하며, 주로 시스템에서 전역적으로 접근 가능한 유일한 객체가 필요할 때 사용된다.
singleton 패턴은 언제 사용하는가 ?
싱글톤 패턴은 객체가 시스템 내에서 단 하나만 존재해야 할 때 사용합니다. 이 패턴은 전역적으로 공유되는 객체가 필요하거나, 여러 곳에서 동일한 인스턴스를 참조해야 할 때 유용하다.
singleton 패턴 적용 코드
싱글톤으로 사용할 클래스 정의. 예제의 상황은 시스템에 전역적으로 적용될 테마색(light,dark)에 대한 설정을 진행할 클래스와 해당 클래스를 이용하여 테마 색이 적용되는 버튼, 텍스트필드, 라벨에 대한 상황을 보겠다.
// 싱글톤으로 사용할 클래스
public class Theme {
// 자기자신의 타입의 멤버변수를 선언한다.
// static 키워드를 사용하는 이유는 전역적으로 하나만 사용하기 위함에 있음
private static Theme instance;
// 테마 칼라를 선언한다.
private String themeColor;
// 생성자가 private인 이유는, 외부에서 해당 클래스 객체를 new 연산자로
// 생성하지 못하게 하기 위함에 있다.
private Theme() {
// 기본적으로 해당 객체가 생성되었을때는 테마 색은 light로 설정한다.
this.themeColor = "light"; // Default theme
}
// 외부에서 객체를 생성하기 위해서는 해당 메서드를 이용해서
// 인스턴스를 반환받아야 한다.
public static Theme getInstance() {
// 만약 클래스의 instance 변수가 null 인경우
// 객체가 생성되어 있지 않다는 걸로 간주하고 new 연산자를 통해 내부에서 객체 생성
if (instance == null) {
instance = new Theme();
}
// 만약 instance 객체가 생성되어 있다면 주소값만 리턴
return instance;
}
// 테마 색을 얻는 메서드
public String getThemeColor() {
return themeColor;
}
// 테마 색을 설정하는 메서드
public void setThemeColor(String themeColor) {
this.themeColor = themeColor;
}
}
각 버튼과 텍스트필드 라벨에 대한 클래스를 정의하고 해당 클래스의 테마색은 싱글톤 클래스의 (Theme) 설정에 따라 display 메서드에서 themeColor가 다르게 출력된다. (색을 따로 dark로 변경하기 전까지는 light가 기본값)
// 싱글톤 클래스에서 값을 참조할 클래스
public class Button {
private String label;
public Button(String label) {
this.label = label;
}
// Theme.getInstance().getThemeColor(); 메서드가 실행 가능한 이유는
// getInstance() 메서드가 static 메서드이기 때문에 객체 생성없이도 호출이 가능하다.
// 전역적으로 하나만 존재하기에 getThemeColor 메서드 호출값은 어디서 호출하던지 동일한 값을 보장함
public void display() {
String themeColor = Theme.getInstance().getThemeColor();
System.out.println(
"Button [" + label + "] displayed in " + themeColor + " theme."
);
}
}
public class TextField {
private String text;
public TextField(String text) {
this.text = text;
}
// 위 주석과 같은 맥락으로 static 메서드 이기 때문에 Theme.getInstance().getThemeColor(); 실행이 가능
public void display() {
String themeColor = Theme.getInstance().getThemeColor();
System.out.println(
"TextField [" + text + "] displayed in " + themeColor + " theme."
);
}
}
public class Label {
private String text;
public Label(String text) {
this.text = text;
}
// 위 주석과 같은 맥락으로 static 메서드 이기 때문에 Theme.getInstance().getThemeColor(); 실행이 가능
public void display() {
String themeColor = Theme.getInstance().getThemeColor();
System.out.println(
"Label [" + text + "] displayed in " + themeColor + " theme."
);
}
}
클라이언트 코드에서는 버튼, 텍스트필드, 라벨에 대한 객체를 생성하고 display 메서드를 호출한다. 간단하게 싱글톤 클래스에서 테마 색을 관리하고 있기에, Theme.getInstance().setThemeColor("dark"); 라인 이후에 다시 display 메서드를 호출하면 dark가 출력된다. 이와 같이 싱글톤 패턴으로 시스템 전역에 적용되어야 하는 설정값을 하나의 객체로 관리 할 수 있다.
// Client code
public class Main {
public static void main(String[] args) {
Button button = new Button("Submit");
TextField textField = new TextField("Enter your name");
Label label = new Label("Username");
// display 메서드는 내부적으로 테마 색을 출력하도록 되어있음
// 기본적으로 light가 기본값이기에, 이시점에서는 light로 출력됨
button.display();
textField.display();
label.display();
// 테마 색을 dark로 변경
Theme.getInstance().setThemeColor("dark");
// 아래 출력문은 테마색이 dark로 변경되어 출력됨
button.display();
textField.display();
label.display();
}
}
singleton 패턴 요약
싱글톤 패턴(Singleton Pattern)은 생성 패턴(Creational Pattern) 중 하나로, 특정 클래스의 인스턴스가 오직 하나만 존재하도록 보장하고, 이 인스턴스에 전역적으로 접근할 수 있게 해주는 디자인 패턴이다. 하지만 위 패턴을 실무에 적용할때는 조금 조심해서 적용해야하는게 멀티스레드 환경에서는 예상치 못한 동기화 문제를 발생시키는 경우도 있으니 조심해서 적용하도록 하자.
-
[baekjoon] 팩토리얼 2 (백준 27433 java 풀이)
intro : 재귀함수는 내가 제일 싫어하는데, 드디어 하기싫은 Chapter 들이 등장하는군.
백준 문제링크
문제
0보다 크거나 같은 정수 N이 주어진다. 이때, N!을 출력하는 프로그램을 작성하시오.
입력
첫째 줄에 정수 N(0 ≤ N ≤ 20)이 주어진다.
출력
첫째 줄에 N!을 출력한다.
문제 풀이 (104ms)
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter, StringBuilder 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
// 재귀함수 factorial 메서드 실행
long result = factorial(Integer.parseInt(br.readLine()));
// 반환된 결과값 append()
sb.append(result);
// bw로 출력하기 위해 문자열로 변환 후 write
bw.write(sb.toString());
bw.flush();
// 자원 반납
bw.close();
br.close();
}
// N! 팩토리얼을 계산할 재귀함수 메서드
// return 타입이 long인 이유는 20!은 int 타입으로 담을 수 있는 크기의 값이 아니다.
public static long factorial(int n) {
// 만약 입력값이 1보다 작다면 1을 return
if (n <= 1) {
return 1;
}
// n * (자기자신의 메서드를 호출한다 (n-1)).
return n * factorial(n - 1);
}
}
문제 해석
드디어 재귀함수 Chapter까지 단계가 올라왔다. 재귀함수는 싫어하는 이유가 3차원적으로 생각하고 로직을 구성해야해서 항상 이 부분이 싫은건데 이번 문제는 그래도 간단하게 구현할 수 있는 문제라서 다행이라고 생각했다.
factorial의 재귀함수 실행순서는 어떻게 되는가 ?
메서드의 재귀함수 호출의 순서는 다음과 같다. 만약 10!의 값을 게산한다면 다음과같은 과정을 거쳐서 결과값을 도출하게 된다. return 문에서 계속해서 자기자신의 메서드를 (n -1)을 한 값으로 호출하게 되는데 n이 1이 되는 시점에서 모든 메서드의 return문이 실행되면서 계산이 마무리 된다.
// 각 단계에서 메서드가 종료된게 아님,
// return 문에서 새로운 factorial을 호출할때마다 해당 메서드 결과값 반환을 기다리고 있음
factorial(10) 호출 -> 10 * factorial(9)
factorial(9) 호출 -> 9 * factorial(8)
factorial(8) 호출 -> 8 * factorial(7)
factorial(7) 호출 -> 7 * factorial(6)
factorial(6) 호출 -> 6 * factorial(5)
factorial(5) 호출 -> 5 * factorial(4)
factorial(4) 호출 -> 4 * factorial(3)
factorial(3) 호출 -> 3 * factorial(2)
factorial(2) 호출 -> 2 * factorial(1)
factorial(1) 호출 -> 종료 조건에 의해 1 반환
// 각 단계에서 값을 반환하고 차례대로 계산 진행
최종적으로 factorial(10)이 3628800을 반환
재귀 함수로 풀지않아도 풀 수 있는 문제이기는 하다.
팩토리얼 문제처럼 while문을 통해서도 충분히 풀 수 있다, 아니면 아래처럼 배열로도 충분히 풀 수 있는 문제이기는 하다. 하기 코드는 팩토리얼의 특성을 이용해서 푼 코드인데, factorial(2) = 2 * factorial(1), factorial(3) = 3 * factorial(2), factorial(4) = 4 * factorial(3), factorial(5) = 5 * factorial(4)… 위와같은 과정을 배열로 구현한 것이다.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
// 입력을 위한 BufferedReader 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// 0 부터 20까지 인덱스가 필요하기에 21개로 배열설정
long[] array = new long[21];
// 팩토리얼은 0과 1은 1값임
array[0] = array[1] = 1;
// 팩토리얼의 특징을 이용해서 배열 인덱스마다 팩토리얼 결과값을 할당
for (int i = 2; i < 21; i++) array[i] = i * array[i - 1];
// 결과값 출력
System.out.println(array[Integer.parseInt(br.readLine())]);
// 자원 반납
br.close();
}
}
마무리
재귀함수 Chapter 이지만 재귀험수로 풀고싶지가 않다. 그냥 디버깅도 쉽고 직관적으로 반복문으로 풀면 안되나. 다음에 푸는 재귀함수 문제에서는 재귀함수를 꼭 써야지만 얻을 수 있는 메리트가 있는지 확인해 보고싶다. 이번 문제에서는 속도차이도 없고 더 나은점을 모르겠다.
-
[baekjoon] 좌표 압축 (백준 18870 java 풀이)
intro : 간만에 문제에서 뭘 원하는지 모르겠는 문제가 나왔다.
백준 문제링크
문제
수직선 위에 N개의 좌표 X1, X2, …, XN이 있다. 이 좌표에 좌표 압축을 적용하려고 한다. Xi를 좌표 압축한 결과 X’i의 값은 Xi > Xj를 만족하는 서로 다른 좌표 Xj의 개수와 같아야 한다. X1, X2, …, XN에 좌표 압축을 적용한 결과 X’1, X’2, …, X’N를 출력해보자.
입력
첫째 줄에 N이 주어진다. 둘째 줄에는 공백 한 칸으로 구분된 X1, X2, …, XN이 주어진다.
출력
첫째 줄에 X’1, X’2, …, X’N을 공백 한 칸으로 구분해서 출력한다.
문제 풀이 (1772ms)
import java.io.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter, StringBuilder 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
// 입력값의 개수
int forCount = Integer.parseInt(br.readLine());
// 원데이터를 저장할 배열 변수
int[] originalArray = new int[forCount];
// 정렬데이터를 저장할 배열 변수
int[] sortArray = new int[forCount];
// 입력값을 공백기준으로 나누기 위한 StringTokenizer 객체 생성
StringTokenizer st = new StringTokenizer(br.readLine());
// 반복문을 통해 originalArray, sortArray에 데이터 할당
for (int i = 0; i < forCount; i++) {
String line = st.nextToken();
originalArray[i] = Integer.parseInt(line);
sortArray[i] = Integer.parseInt(line);
}
// 배열 오름차순으로 정렬
Arrays.sort(sortArray);
int index = 0;
// Map<정렬데이터, index(순서)> 로 저장하기 위한 map 변수 선언
Map<Integer, Integer> map = new HashMap<>();
for (int value : sortArray) {
// 중복된 데이터인 경우는 거르기 위함
if (!map.containsKey(value)) {
map.put(value, index);
index++;
}
}
// 원데이터를 몇번째 인덱스에 해당하는지 map 에서 찾기
for (int ori : originalArray) {
// sb.append() 메서드를 통해 저장
sb.append(map.get(ori)).append(" ");
}
// bw로 출력하기 위해 문자열로 변환 후 write
bw.write(sb.toString());
bw.flush();
// 자원 반납
bw.close();
br.close();
}
}
문제 해석
간만에 문제 자체만으로 무슨말인지 도저히 모르겠는 문제가 나왔다. 가끔 이런 문제를 보면 문제를 너무 성의없이 내는게 아닌가 싶을때가 있는데, 딱 이런문제가 그런 문제다. 정확히 무엇을 원하는지가 명확하지가 않다. 입력예제랑 출력을 보고 문제에서 원하는 바를 추측해 보았다.
[입력 예제 1]
5
2 4 -10 4 -9
------------------
[예제 출력 1]
2 3 0 3 1
------------------
[입력 예제 1 정렬 (오름차순)]
-10(0), -9(1), 2(2), 4(3), 4(4)
문제 추측
딱 위 내용을 보니까 대충 문제에서 원하는 바가 정렬한 값이 원래 값에 맞춰서 인덱스 값을 추출하는걸 원하는 거구나 싶었다. 다만 중복된 값을 하나의 인덱스만을 가지는거 같았다, 입력예제 1번에서의 4는 두번등장하는데 출력된 결과를 보면 3이 2번 출력된다. 만약 중복을 포함하는거라면 3,4 로 값이 나왔을거 같은데 그게 아닌거보니 중복을 제거해서 인덱스를 계산해야한다는 점을 알 수 있다.
(과연 이런식으로 추측해서 문제를 푸는게 맞나싶은데, 정말이지 이 문제는 문제 텍스트가 너무 성의가 없는거 같다.)
구현 방향
이 문제의 핵심은 주어진 좌표들을 압축하여 정수 인덱스로 변환하는 것 같았다. 이해한 내용을 기반으로 좌표 간의 상대적 크기를 반영하여 배열을 생성하였고 원데이터와, 정렬한 데이터를 두개로 나누어 관리하였다. 그후에 정렬한 데이터를 map 변수에 저장을 하는데, 이때 중복된 데이터를 저장하지 않기 위해 검증로직을 추가하였다. containsKey 메서드는 시간복잡도가 1이라 빨라서 나는 자주 애용한다.
그 이후 원데이터를 반복문을 돌면서 원데이터의 값을 map에서 꺼낸다, Hash값이기 떄문에 이 또한 시간 복잡도는 1이라 빠르게 값을 찾아서 StringBuilder에 값을 추가할 수 있다.
마무리
문제 자체를 정확히 이해하지 못하고 풀어서 뭔가 다양한 관점으로 보기 어려웠던게 좀 많이 아쉽다. 그리고 시간이 굉장히 오래걸리길래 다른 분들 풀이 시간을 확인해보았는데, 다들 1초 ~ 2초 사이를 소요하고 있으신거 같았다. 이 문제 특성상 입력값이 크게주어져서 시간이 굉장히 오래 소요 되는 것 같았다.
-
[baekjoon] 나이순 정렬 (백준 10814 java 풀이)
intro : static 내부 class를 통해 문제를 풀어보자
백준 문제링크
문제
온라인 저지에 가입한 사람들의 나이와 이름이 가입한 순서대로 주어진다. 이때, 회원들을 나이가 증가하는 순으로, 나이가 같으면 먼저 가입한 사람이 앞에 오는 순서로 정렬하는 프로그램을 작성하시오.
입력
첫째 줄에 온라인 저지 회원의 수 N이 주어진다. (1 ≤ N ≤ 100,000) 둘째 줄부터 N개의 줄에는 각 회원의 나이와 이름이 공백으로 구분되어 주어진다. 나이는 1보다 크거나 같으며, 200보다 작거나 같은 정수이고, 이름은 알파벳 대소문자로 이루어져 있고, 길이가 100보다 작거나 같은 문자열이다. 입력은 가입한 순서로 주어진다.
출력
첫째 줄부터 총 N개의 줄에 걸쳐 온라인 저지 회원을 나이 순, 나이가 같으면 가입한 순으로 한 줄에 한 명씩 나이와 이름을 공백으로 구분해 출력한다.
문제 풀이 (548ms)
import java.io.*;
import java.util.Arrays;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter, StringBuilder 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
// 입력값의 개수
int forCount = Integer.parseInt(br.readLine());
// Member 배열 선언
Member[] members = new Member[forCount];
// 반복문을 통해 Member 객체를 배열로 저장
for (int i = 0; i < forCount; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
// 이때 가입한 순서를 i로 Member 클래스의 index 값을 저장
members[i] = new Member(Integer.parseInt(st.nextToken()), st.nextToken(), i);
}
// 배열 정렬
Arrays.sort(members, (o1, o2) -> {
// 나이값을 기준으로 오름차순 정렬
if (o1.age != o2.age) {
return o1.age - o2.age;
} else {
// 나이값이 같다면 가입한 순서로 오름차순 정렬
return o1.index - o2.index;
}
});
// 정렬된 members 데이터 sb.append() 메서드를 통해 저장
for (Member member : members) {
sb.append(member.age).append(" ").append(member.name).append("\n");
}
// bw로 출력하기 위해 문자열로 변환 후 write
bw.write(sb.toString());
bw.flush();
// 자원 반납
bw.close();
br.close();
}
// Member 클래스 생성
// static 으로 생성하는 이유는 내부 클래스는 static 으로 생성해야
// 메모리 관점에서 효율적으로 관리할 수 있고, 누수를 막을 수 있다.
static class Member {
int age; // 나이
String name; // 이름
int index; // 가입 순서
// 생성자를 통한 객체 생성
public Member(int age, String name, int index) {
this.age = age;
this.name = name;
this.index = index;
}
}
}
문제 해석
이 문제는 기존에 풀던 정렬문제랑 크게 차이가 없었는데 관리해야하는 값의 개수가 2개에서 3개로 늘어남에 따라 어떻게 처리하는게 좋을지 생각하다가 배열을 통해 관리를 하는게 어떨지 생각하고 다음과 같이 문제를 풀어보았었다.
856ms 소요
import java.io.*;
import java.util.Arrays;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter, StringBuilder 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
// 입력값의 개수
int forCount = Integer.parseInt(br.readLine());
// 나이, 이름, 가입순서를 저장할 배열 선언
String[][] array = new String[forCount][3];
// 반복문을 통해 array 데이터 저장
for (int i = 0; i < forCount; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
array[i][0] = st.nextToken();
array[i][1] = st.nextToken();
array[i][2] = Integer.toString(i);
}
// 배열 정렬
Arrays.sort(array, (o1, o2) -> {
// 나이값을 기준으로 오름차순 정렬
if (!o1[0].equals(o2[0])) {
return Integer.parseInt(o1[0]) - Integer.parseInt(o2[0]);
} else {
// 나이값이 같다면 가입한 순서로 오름차순 정렬
return Integer.parseInt(o1[2]) - Integer.parseInt(o2[2]);
}
});
// 정렬된 array 데이터 sb.append() 메서드를 통해 저장
for (String[] arr : array) {
sb.append(arr[0]).append(" ").append(arr[1]).append("\n");
}
// bw로 출력하기 위해 문자열로 변환 후 write
bw.write(sb.toString());
bw.flush();
// 자원 반납
bw.close();
br.close();
}
}
static class를 사용하게된 이유
근데 위와같이 풀고나니까, 정렬하는 부분에서 각 값을 자꾸 형변환을 해줘야하는게 너무 신경이 쓰였다. 뭐 이대로 풀어도 정답처리되는 코드이지만 뭔가 석연치않는 부분이 남는거 같다는 생각이 들었다. 아무래도 정수형 데이터와 문자열 데이터를 같이 관리해야하다보니, 다른 자료구조를 사용하는게 좋을거 같다는 생각이 들었는데, 객체를 통해 자료구조를 만들어 보는게 어떨까? 라는 생각이 들었다.
static class를 사용하면서 얻게 되는 이점
가장 큰 이점은 형변환을 하지 않아도 되는점이다. 아무래도 정렬 로직에서 각 값을 계속해서 Integer.parseInt를 하는게 시간복잡도 관점에서 크게 영향을 미치기도하고, 배열을 통해 3개의 데이터를 0,1,2 번 인덱스에 값을(나이,이름,가입순서) 할당하는게 누군가 보았을때 유지보수 관점에서도 크게 좋지 않는데 내부 클래스를 이용해서 객체로 관리하니 유연성 및 확장성 에서도 이점을 가져올수 있고 가독성 부분에서도 굉장히 쉽게 읽히는 부분이 생긴다.
마무리
처음으로 알고리즘 문제에서 객체를 활용해 문제를 풀어봤는데, 다음에도 혹시 관리포인트가 늘어나는 자료구조가 필요하다면 기존에 자바에서 제공해주는 자료구조 및 객체도 고민거리에 포함해서 생각해 보아야 겠다고 생각이 들었다.
-
[baekjoon] 단어 정렬 (백준 1181 java 풀이)
intro : TreeSet을 사용하면, 중복제거 및 정렬 기능을 동시에 이점을 가져올 수 있다.
백준 문제링크
문제
알파벳 소문자로 이루어진 N개의 단어가 들어오면 아래와 같은 조건에 따라 정렬하는 프로그램을 작성하시오.길이가 짧은 것부터 길이가 같으면 사전 순으로 단, 중복된 단어는 하나만 남기고 제거해야 한다.
입력
첫째 줄에 단어의 개수 N이 주어진다. (1 ≤ N ≤ 20,000) 둘째 줄부터 N개의 줄에 걸쳐 알파벳 소문자로 이루어진 단어가 한 줄에 하나씩 주어진다. 주어지는 문자열의 길이는 50을 넘지 않는다.
출력
조건에 따라 정렬하여 단어들을 출력한다.
문제 풀이 (308ms)
import java.io.*;
import java.util.Set;
import java.util.TreeSet;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter, StringBuilder 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
// 입력값의 개수
int forCount = Integer.parseInt(br.readLine());
// 중복 제거를 위한 Set 변수 선언
// TreeSet 의 특징은 중복을 제거하고, 정렬기능을 제공한다.
// 또한 특정 정렬 기준을 적용한 TreeSet 을 설정할 수 있다.
Set<String> set = new TreeSet<>((o1, o2) -> {
// 문자길이가 짧은게 앞에오도록 오름차순 정렬
if (o1.length() != o2.length()) {
return o1.length() - o2.length();
} else {
// 문자길이가 같을때, 사전순으로 정렬될 수 있도록 정렬
return o1.compareTo(o2);
}
});
// 입력값을 set 변수에 저장
for (int i = 0; i < forCount; i++) set.add(br.readLine());
// 정렬된 set 데이터 sb.append() 메서드를 통해 저장
for (String str : set) sb.append(str).append("\n");
// bw로 출력하기 위해 문자열로 변환 후 write
bw.write(sb.toString());
bw.flush();
// 자원 반납
bw.close();
br.close();
}
}
문제 해석
정렬하는 문제를 좀 풀어봤다 싶지만, TreeSet을 통해 푸는 방법이 굉장히 좋은거 같다. 중복제거를 해줌과 동시에 정렬기준또한 변수 선언시에 정할 수 있다. 처음에는 위 방식으로 코드를 작성했던 것이 아닌 아래 코드처럼 List를 통한 정렬을 했었다.
328ms 소요
import java.io.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter, StringBuilder 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
// 입력값의 개수
int forCount = Integer.parseInt(br.readLine());
// 중복 제거를 위한 Set 변수 선언
Set<String> set = new HashSet<>();
for (int i = 0; i < forCount; i++) {
set.add(br.readLine());
}
// 정렬하기 위한 List 변수 선언 (Set 을 통해 생성)
List<String> list = new ArrayList<>(set);
// 정렬 진행
list.sort((o1, o2) -> {
// 문자열 길이기준으로 오름차순 정렬 (짧으면 앞으로 옴)
if (o1.length() != o2.length()) {
return o1.length() - o2.length();
} else {
// 길이가 같으면 사전순으로 정렬
return o1.compareTo(o2);
}
});
// 반복문을 통해 sb.append() 메서드 호출
for (String str : list) {
sb.append(str).append("\n");
}
// bw로 출력하기 위해 문자열로 변환 후 write
bw.write(sb.toString());
bw.flush();
// 자원 반납
bw.close();
br.close();
}
}
기존에도 많이 사용하던 방법이라, 이질감 없이 코드를 작성하고 문제를 풀어냈는데, 중복을 제거하고 정렬하는 부분이 다른 자료구조를 사용하면 더 편하게 처리할 수 있을거 같다는 생각이 들어서 찬찬히 생각해보니, TreeSet 자료구조가 있었다.
왜 TreeSet을 사용하는가 ?
TreeSet은 다음과 같은 특징이 있다. 중복을 제거하며, 정렬한 데이터를 제공해준다. 추가적으로 정렬을 하는 기준을 내가 커스터마이징하여 지정할 수 있다. 비슷한 자료구조 중에 PriorityQueue에도 정렬기능을 커스터마이징 하는게 있는데 TreeSet 또한 그렇다.
TreeSet을 사용하면서 얻는 이점
TreeSet을 사용하니 기존에 코드에서 다음과 같은 이점이 생긴다. Set에서 List로 변환할 필요가 없다. List를 통해 따로 Sort() 메서드를 호출할 필요가 없다. 또한 코드가 간결해지며 직관적으로 작성된다.
마무리
문제에 따라 사용하는 자료구조에 따라, 수행시간과 코드 가독성에도 큰 영향을 미치는게 위 문제를 통해 많이 느껴진다.
-
[baekjoon] 좌표 정렬하기 2 (백준 11651 java 풀이)
intro : 좌표 정렬하기 문제 방금풀었는데, 그 문제랑 다른건 x,y 정렬 기준 뿐이다.
백준 문제링크
문제
2차원 평면 위의 점 N개가 주어진다. 좌표를 y좌표가 증가하는 순으로, y좌표가 같으면 x좌표가 증가하는 순서로 정렬한 다음 출력하는 프로그램을 작성하시오.
입력
첫째 줄에 점의 개수 N (1 ≤ N ≤ 100,000)이 주어진다. 둘째 줄부터 N개의 줄에는 i번점의 위치 xi와 yi가 주어진다. (-100,000 ≤ xi, yi ≤ 100,000) 좌표는 항상 정수이고, 위치가 같은 두 점은 없다.
출력
첫째 줄부터 N개의 줄에 점을 정렬한 결과를 출력한다.
문제 풀이 (596ms)
import java.io.*;
import java.util.Arrays;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter, StringBuilder 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
// 입력값의 개수
int forCount = Integer.parseInt(br.readLine());
// 좌표값을 저장할 2차원 배열 선언
int[][] array = new int[forCount][2];
// 빈복문을 통해 2차원 배열에 데이터 저장
for (int i = 0; i < forCount; i++) {
// 입력값을 공백 기준으로 데이터를 자르기 위한 StringTokenizer 객체 생성
StringTokenizer st = new StringTokenizer(br.readLine());
array[i][0] = Integer.parseInt(st.nextToken());
array[i][1] = Integer.parseInt(st.nextToken());
}
// 배열 정렬
Arrays.sort(array, (o1, o2) -> {
// y값이 다르다면 y값을 기준으로 오름차순 정렬
if (o1[1] != o2[1]) {
return o1[1] - o2[1];
} else {
// y값이 같다면 x값을 기준으로 오름차순 정렬
return o1[0] - o2[0];
}
});
// 정렬된 2차원 배열 데이터 sb.append() 메서드를 통해 저장
for (int[] arr : array) {
sb.append(arr[0])
.append(" ")
.append(arr[1])
.append("\n");
}
// bw로 출력하기 위해 문자열로 변환 후 write
bw.write(sb.toString());
bw.flush();
// 자원 반납
bw.close();
br.close();
}
}
문제 해석
이 문제는 좌표 정렬하기 문제와 정렬기준이 반대인거 말고는 차이가 1도 없다. 정렬기준이 반대라는 점은 이전 좌표정렬하기 문제에서는 x값 기준으로 먼저 정렬하고 x값이 같다면 y값을 기준으로 오름차순 정렬을 하였는데, 이번 문제는 y값 기준으로 먼저 정렬하고 y값이 같다면 x값 기준으로 오름차순 정렬을 하는것으로 차이가 있다.
중요로직 (정렬)
아래로직은 2차원 배열을 y값을 기준으로 오름차순 정렬하고, y값이 같다면 x값을 기준으로 정렬하는 코드이다. 아래 코드가 이 문제를 푸는 키 포인트 이다.
import java.io.*;
import java.util.Arrays;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
...
...
// 배열 정렬
Arrays.sort(array, (o1, o2) -> {
// y값이 다르다면 y값을 기준으로 오름차순 정렬
if (o1[1] != o2[1]) {
return o1[1] - o2[1];
} else {
// y값이 같다면 x값을 기준으로 오름차순 정렬
return o1[0] - o2[0];
}
});
...
...
}
}
-
[baekjoon] 좌표 정렬하기 (백준 11650 java 풀이)
intro : List를 통해서도 풀 수 있지만 값의 범위가 지정되어 있으니 2차원 배열로도 풀 수 있다.
백준 문제링크
문제
2차원 평면 위의 점 N개가 주어진다. 좌표를 x좌표가 증가하는 순으로, x좌표가 같으면 y좌표가 증가하는 순서로 정렬한 다음 출력하는 프로그램을 작성하시오.
입력
첫째 줄에 점의 개수 N (1 ≤ N ≤ 100,000)이 주어진다. 둘째 줄부터 N개의 줄에는 i번점의 위치 xi와 yi가 주어진다. (-100,000 ≤ xi, yi ≤ 100,000) 좌표는 항상 정수이고, 위치가 같은 두 점은 없다.
출력
첫째 줄부터 N개의 줄에 점을 정렬한 결과를 출력한다.
문제 풀이 (640ms)
import java.io.*;
import java.util.Arrays;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter, StringBuilder 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
// 입력값의 개수
int forCount = Integer.parseInt(br.readLine());
// 좌표값을 저장할 2차원 배열 선언
int[][] inputArray = new int[forCount][2];
// 빈복문을 통해 2차원 배열에 데이터 저장
for (int i = 0; i < forCount; i++) {
// 입력값을 공백 기준으로 데이터를 자르기 위한 StringTokenizer 객체 생성
StringTokenizer st = new StringTokenizer(br.readLine());
inputArray[i][0] = Integer.parseInt(st.nextToken());
inputArray[i][1] = Integer.parseInt(st.nextToken());
}
// 배열 정렬
Arrays.sort(inputArray, (o1, o2) -> {
if (o1[0] != o2[0]) {
// x값이 다르다면 x값을 기준으로 오름차순 정렬
return o1[0] - o2[0];
} else {
// x값이 같다면 y값을 기준으로 오름차순 정렬
return o1[1] - o2[1];
}
});
// 정렬된 2차원 배열 데이터 sb.append() 메서드를 통해 저장
for (int[] arr : inputArray) {
sb.append(arr[0])
.append(" ")
.append(arr[1])
.append("\n");
}
// bw로 출력하기 위해 문자열로 변환 후 write
bw.write(sb.toString());
bw.flush();
// 자원 반납
bw.close();
br.close();
}
}
문제 해석
이 문제를 풀기전에 다른 비슷한 정렬 문제를 하나 풀어보니까 이제는 이런 문제나오면 반갑다.. 혹시 그 문제가 무엇인지 궁금하다면 영단어 암기는 괴로워 를 풀어보는걸 추천한다. 이문제랑 결이 비슷하면서도 조금 더 ? 어주 살짝 더 어려운 문제인거 같다.
문제 해석 방향
이 문제는 정렬의 조건이 오름차순으로 정렬해야 하는것으로 굉장히 명확하다, x좌표가 증가하는 순으로, x좌표가 같으면 y좌표가 증가하는 순서로 정렬 이라는 조건이 문제에서 주어지는데, 그렇다면 해당 정렬을 하기 위해 어떤 자료구조를 사용할지 선택만하면 된다. 초기에는 2차원 배열을 통해 데이터를 관리했던것은 아니고, List를 통해 데이터를 관리했는데, 다음과 같이 코드를 작성하여 문제를 풀었었다.
620ms 소요
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter, StringBuilder 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
// 좌표값을 저장할 리스트 선언 (타입은 1차원 배열)
List<int[]> list = new ArrayList<>();
// 입력값의 개수
int forCount = Integer.parseInt(br.readLine());
// 빈복문을 통해 리스트에 데이터 저장
for (int i = 0; i < forCount; i++) {
// 입력값을 공백 기준으로 데이터를 자르기 위한 StringTokenizer 객체 생성
StringTokenizer st = new StringTokenizer(br.readLine());
// 리스트에 저장할 1차원 배열 객체 생성
int[] tempArray = new int[2];
tempArray[0] = Integer.parseInt(st.nextToken());
tempArray[1] = Integer.parseInt(st.nextToken());
list.add(tempArray);
}
// 리스트 정렬
list.sort((o1, o2) -> {
if (o1[0] != o2[0]) {
// x값이 다르다면 x값을 기준으로 오름차순 정렬
return o1[0] - o2[0];
} else {
// x값이 같다면 y값을 기준으로 오름차순 정렬
return o1[1] - o2[1];
}
});
// 정렬된 리스트 sb.append() 메서드를 통해 저장
for (int[] intArray : list) {
sb.append(intArray[0])
.append(" ")
.append(intArray[1])
.append("\n");
}
// bw로 출력하기 위해 문자열로 변환 후 write
bw.write(sb.toString());
bw.flush();
// 자원 반납
bw.close();
br.close();
}
}
List 타입으로 문제 잘 풀어놓고 왜 배열로 리팩토링을 하였는가 ?
항상 알고리즘 문제를 풀때 다양한 관점으로 문제를 풀어보려 하는데, 해당 문제는 배열로 풀 수 있는 가능성이 있었다. 그 이유는 입력값으로 정확히 배열의 사이즈를 지정할 수 있는 개수를 초기에 지정해주기에 배열로 선언해서 사이즈를 지정할 수 있었고, 해당 문제의 시간복잡도는 배열의 정렬을 어떻게 하는냐에 따라서 시간적으로 차이가 많이 날 것이라고 생각했다. 왜냐하면, Arrays.sort()는 List.sort()보다 이론적으로 빠르게 작동하기에 당연히 배열을 통해 문제를 푸는게 더 나을거라고 생각했다.
Arrays.sort()가 List.sort()보다 왜 더 빠르게 작동하는가 ?
보통 성능적으로 크게 차이가 나지 않을거라고 생각하지만 입력값의 크기가 커지면 커질수록 성능의 차이는 더 커진다. 그 이유는 List.sort()는 내부적으로 컬렉션을 배열로 변환하는 과정을 거친다. 그 이후 배열의 대한 정렬(Arrays.sort())를 진행하고 다시 List로 변환한다. 해당 과정을 거치는 이유 때문에 Arrays.sort()가 보다 더 빠르게 동작한다. (동작 단계로만 따져봐도 누가 더 빠르게 동작할지 알 수 있다.)
그래서 두개의 코드 실행 시간은 차이가 얼마인가 ?
이론적으로는 2차원 배열로 푸는것이 빠르다고 생각했는데 실제로는 리스트가 더 빨랐다. 여러번 실행해보니 거의 속도적인 차이는 없었는데, 그 이유는 문제에서 주어진 입력값의 개수가 크지않기에 속도적인 차이가 나지않았다. 아무래도 100,000개의 데이터를 처리하는 코드에 있어서는 큰 차이를 볼수없고, 몇백만개정도는 되어야 두개의 자료구조를 통한 풀이가 속도 차이가 있을 것 같다.
혹시 정렬코드 return o1 - o2 가 이해가 안된다면?
다른 방식으로도 정렬기준을 정할 수 있다. 다음과 같이 코드를 작성할 수 도 있고 사실상 해당 문제에 한해서는 속도적인 차이가 거의 나지를 않으니 본인 코드 스타일에 맞게 선택 하면 될 것 같다.
616ms 소요
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter, StringBuilder 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
// 좌표값을 저장할 리스트 선언 (타입은 1차원 배열)
List<int[]> list = new ArrayList<>();
// 입력값의 개수
int forCount = Integer.parseInt(br.readLine());
// 빈복문을 통해 리스트에 데이터 저장
for (int i = 0; i < forCount; i++) {
// 입력값을 공백 기준으로 데이터를 자르기 위한 StringTokenizer 객체 생성
StringTokenizer st = new StringTokenizer(br.readLine());
// 리스트에 저장할 1차원 배열 객체 생성
int[] tempArray = new int[2];
tempArray[0] = Integer.parseInt(st.nextToken());
tempArray[1] = Integer.parseInt(st.nextToken());
list.add(tempArray);
}
// 리스트 정렬
list.sort((o1, o2) -> {
// x 값 기준으로의 오름차순 정렬
if (o1[0] < o2[0]) {
return -1;
} else if (o1[0] == o2[0]) {
// x 값이 같다면 y값 기준으로의 오름차순 정렬
if (o1[1] < o2[1]) {
return -1;
} else if (o1[1] == o2[1]) {
return 0;
} else {
return 1;
}
} else {
return 1;
}
});
// 정렬된 리스트 sb.append() 메서드를 통해 저장
for (int[] intArray : list) {
sb.append(intArray[0])
.append(" ")
.append(intArray[1])
.append("\n");
}
// bw로 출력하기 위해 문자열로 변환 후 write
bw.write(sb.toString());
bw.flush();
// 자원 반납
bw.close();
br.close();
}
}
마무리
왜 리팩토링을 하면 할수록 속도가 더 느려지는지는 의문이지만(미비하게나마 느려진다), 분명히 이론적으로는 Arrays.sort()가 빠르다고했는데… 그냥 직관적으로 단순하게 풀어버릴걸 그랬나 싶기도 하지만 다양하게 테스트 해보고 왜 빠르고 느린지에 대해서 스스로 탐구하고 이해하는 과정이 나에게 큰 도움이 되었던 것 같다.
-
[baekjoon] 영단어 암기는 괴로워 (백준 20920 java 풀이)
intro : 정렬은 항상 고려할게 많아서 어렵다. 정렬 방법과 종류도 너무많고.. 따로 내용을 정리할 Chapter가 필요할정도다.
백준 문제링크
문제
화은이는 이번 영어 시험에서 틀린 문제를 바탕으로 영어 단어 암기를 하려고 한다. 그 과정에서 효율적으로 영어 단어를 외우기 위해 영어 단어장을 만들려 하고 있다. 화은이가 만들고자 하는 단어장의 단어 순서는 다음과 같은 우선순위를 차례로 적용하여 만들어진다.자주 나오는 단어일수록 앞에 배치한다. 해당 단어의 길이가 길수록 앞에 배치한다. 알파벳 사전 순으로 앞에 있는 단어일수록 앞에 배치한다. M보다 짧은 길이의 단어의 경우 읽는 것만으로도 외울 수 있기 때문에 길이가 M이상인 단어들만 외운다고 한다. 화은이가 괴로운 영단어 암기를 효율적으로 할 수 있도록 단어장을 만들어 주자.
입력
첫째 줄에는 영어 지문에 나오는 단어의 개수 N과 외울 단어의 길이 기준이 되는 M이 공백으로 구분되어 주어진다. (1 <= N <= 100,000, 1 <= M <= 10) 둘째 줄부터 N+1번째 줄까지 외울 단어를 입력받는다. 이때의 입력은 알파벳 소문자로만 주어지며 단어의 길이는 10을 넘지 않는다. 단어장에 단어가 반드시 1개 이상 존재하는 입력만 주어진다.
출력
화은이의 단어장에 들어 있는 단어를 단어장의 앞에 위치한 단어부터 한 줄에 한 단어씩 순서대로 출력한다.
문제 풀이 (700ms)
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter, StringBuilder 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
// 공백을 기준으로 자르기 위한 StringTokenizer 객체 사용
StringTokenizer st = new StringTokenizer(br.readLine());
// 입력값의 개수
int forCount = Integer.parseInt(st.nextToken());
// 입력받아야 하는 단어의 길이 최소값
int minLength = Integer.parseInt(st.nextToken());
// 최빈값을 보관하기 위한 자료구조 Map 사용
Map<String, Integer> map = new HashMap<>();
for (int i = 0; i < forCount; i++) {
String input = br.readLine();
// 최소 길이 이상의 단어만 빈도수를 증가시키며 map 변수에 담음
if (input.length() >= minLength) {
map.put(input, map.getOrDefault(input, 0) + 1);
}
}
// 출력값은 중복을 제거한 단어의 값이기에, map 의 키값을 통해 정렬진행
List<String> list = new ArrayList<>(map.keySet());
list.sort((o1, o2) -> {
Integer first = map.get(o1);
Integer second = map.get(o2);
// 자주 나오는 단어일수록 앞에 배치한다.
if (!Objects.equals(first, second)) {
// 앞에 배치한다는 말은 내림차순 정렬을 한다는 말과 같음
// 만약 return first - second 인 경우는 오름차순 정렬과 같음
return second - first;
}
// 해당 단어의 길이가 길수록 앞에 배치한다.
if (o1.length() != o2.length()) {
// 위 주석과 같은 의미를 가지고 있음
return o2.length() - o1.length();
}
// 알파벳 사전 순으로 앞에 있는 단어일수록 앞에 배치한다
// String은 Comparable 인터페이스를 구현하고 있음
// 제공되는 사전순 정렬 메서드 compareTo를 사용가능
return o1.compareTo(o2);
});
// 정렬된 key 값을 StringBuilder 에 저장
for (String l : list) {
sb.append(l).append("\n");
}
// bw로 출력하기 위해 문자열로 변환 후 write
bw.write(sb.toString());
bw.flush();
// 자원 반납
bw.close();
br.close();
}
}
문제 해석
정렬문제는 항상 손이 많이가는 편인데, 해당 문제는 사용자 정의 정렬을 구현할수 있냐를 물어보는 문제인거 같다는 생각이 들었다. 문제에서 직관적으로 정렬 조건을 명확하게 주어졌는데, 자주 나오는 단어일수록 앞에 배치, 해당 단어의 길이가 길수록 앞에 배치, 알파벳 사전 순으로 앞에 있는 단어일수록 앞에 배치. 결국 이 세가지 정렬 조건을 코드로 구현할 수 있냐를 물어보는 것이다. 너무 별거 아닌거라서 가장 첫번째 조건을 누락할뻔했는데 M이상인 단어들만 외운다 까지 포함해야한다.
부가 조건
주어지는 M 이상의 단어만 입력값으로 받아야함.
정렬 조건 정리
자주 나오는 단어일수록 앞에 배치 : 빈도수를 계산해서 내림차순 정렬을 진행해야한다.
해당 단어의 길이가 길수록 앞에 배치 : 단어의 길이를 계산해서 내림차순 정렬을 진행해야한다.
알파벳 사전 순으로 앞에 있는 단어일수록 앞에 배치 : String Class의 CompareTo를 사용하자.
Map을 사용한 이유
첫번째 정렬조건인 자주 나오는 단어일수록 앞에 배치의 말 뜻은 빈도수를 계산 해야한다는 말이다. 여기서 빈도수는 단어가 입려값으로 몇번이나 나오는가? 를 물어보는것과 같다. 그렇다면 다양한 자료구조를 통해서 빈도수를 계산할 수 있는데, 출력값 예제를 확인해보면 중복된 값은 출력하지 않고 있다. 왜냐하면 외워야하는 단어를 목록을 만들어서 출력하는것이기에 중복된 단어를 포함할 이유가 없다. 또한 두번째 정렬 기준인 단어의 길이를 가지고 정렬조건을 사용하기 위해선 어떤 단어인지를 알아야하는데 Map<단어, 빈도수> 로 변수를 구성해서 관리하면 쉽게 정렬에서 연산과정을 수행할수 있다. 이하동문으로 알파벳 사전순으로 정렬하기도 2번째 조건에서 연산하는것과 같이 Map의 키값에 CompareTo 메서드를 사용하면 되기에 이 문제에서 Map이 적합한 자료구조 타입이라고 생각이 들었다.
정렬 로직에서 return second - first 와 같은 맥락을 사용한 이유 (자주 나오는 단어, 단어의 길이를 앞에 배치할때 사용한 기준)
보통 사용자 정의 정렬 로직을 구현하는 코드를 찾아보면 다음과 같은 코드 로직을 자주 볼 수 있다. 만약 문자열 길이를 기준으로 오름차순으로 정렬해야한다, 라는 문제가 있다면 다음과 같이 정렬하는 로직을 자주 보곤 한다.
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
String[] array = {"a", "b", "c", "ab", "abc", "abed", "addon"};
// 아래 정렬 기준과 return 값을 집중해보자
Arrays.sort(array, (o1, o2) -> {
if (o1.length() < o2.length()) {
return -1;
} else if (o1.length() > o2.length()) {
return 1;
} else {
return 0;
}
});
// 배열을 한번에 출력하기 위해 Arrays.toString 메서드 사용
System.out.println(Arrays.toString(array));
}
}
// 출력값
// [a, b, c, ab, abc, abed, addon]
위처럼 if문 조건을 통해 첫번째 값과 두번째값의 비교를 통해 어떤값을 return하는가? -1,0,1 에 따라 정렬이 달라진다. 만약 위처럼 오름차순이 아니라 내림차순으로 정렬해야한다면 아래와 같이 return값이 반대로만 적용되면 된다.
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
String[] array = {"a", "b", "c", "ab", "abc", "abed", "addon"};
// 아래 정렬 기준과 return 값을 집중해보자
Arrays.sort(array, (o1, o2) -> {
if (o1.length() < o2.length()) {
return 1;
} else if (o1.length() > o2.length()) {
return -1;
} else {
return 0;
}
});
// 배열을 한번에 출력하기 위해 Arrays.toString 메서드 사용
System.out.println(Arrays.toString(array));
}
}
// 출력값
// [addon, abed, abc, ab, a, b, c]
두개의 차이는 같은 조건이면서도 return 되는값이 -1이냐 0이냐 1이냐에 따라서 정렬이 다르게 적용된다는 것이다. 그말은 다르게 풀이해보자면 return 값은 그저 정렬 알고리즘이 두 값을 비교해 위치를 교환하거나 유지하는지 결정하는데 사용하는 기준 값일 뿐이고 실제로는 음수인 값인지 정수인 값인지만 알면 된다는것이다. 이 말을 이해 했다는 가정하에 다시 return second - first; 에 대해서 확인해보면 만약 second 값이 100인데, first가 50 이라면 양수의 값이 return 된다. 그때 second 값이 앞으로 이동하는데 그 이유는 o1, o2에 대한 비교를 진행시에 만약 first - second를 했고 결과값이 양수라면 first가 뒤로 이동하는게 맞는데, 이건 오름차순 정렬일때 해당하는 말이고, 우리는 출력을 내림차순으로 해야하기에 과정을 반대로 뒤집어서 return second - first; 를 이용해야 우리가 원하는 반대의 결과 즉 내림차순 정렬을 할 수 있음을 알 수 있다.
Integer 클래스에도 String Class 처럼 CompareTo 메서드가 있는데 왜 사용을 안하는가?
먼저 아래코드를 확인해보자, Integer 클래스의 일부 코드를 발췌한 것이다.
public final class Integer extends Number implements Comparable<Integer> {
...
...
...
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
...
...
...
}
Integrer 클래스도 String Class 처럼 compareTo 메서드를 제공한다. 그럼에도 불구하고 왜 return second - first; 를 사용했을까? 사실 아주 간단한 이유가 있다. 단순 - 를통한 연산과 내부 메서드를 호출하여 연산 결과를 리턴받는 것과 효율의 관점으로 바라봤을때 당연히 단순 연산이 조금 더 낫다는 판단이 있었다. 메서드를 통해서 풀이를 하는것도 가독성 관점에서 본다면 나쁘찌 않다고 생각하지만, 알고리즘 문제를 푸는 관점에서는 시간복잡도 및 최적의 연산이 가장 우선시 되어야 하기에 단순 - 연산을 통해 코드를 구성했다.
마무리
위 문제를 풀면서 여러 방면으로 고민할 수 있었던 문제였던거 같은데 더 깊게 들어가면 더 깊은 고찰을 해야할거 같아서 이정도로만 정리하고 넘어가고자 한다. 실버4 문제랑 실버3 문제랑 갭차이가 이정도인가 싶은데 갑자기 난이도가 확 올라간 느낌이다.
-
[diary] 점심먹기 전에 자이언티 노래 V 들으면서 글 쓰는 사람
intro : Zion.T V 노래 생각보다 굉장히 좋은걸?
아아. 저번 주말에 .html 확장자로 고통받던 내가 드디어 permalink 지옥으로부터 탈출을 했다. 진짜 지옥이었는데 하다하다 도저히 방법을 못찾을거 같아서 내가 내돈주고 GPT 결제까지 했다. 나보다 똑똑한 인공지능이 문제를 해결해 준다면 3만원 아깝지 않다고 생각했단말이야. 하지만 실상은 내가 프롬프트를 잘 활용을 못하는건지 크게 성능적으로 차이를 못느끼겠다는말이지… 그래서 나의 멍청함을 채워줄 수 있도록 GPT 많이 들들볶고 혼도냈다.
(정말 크게 혼냈어. 말을 안들어.)
여차저차 해결한 방법을 조금 언급해 보면 이게 진짜 맞는 방법인지는 모르겠달까? 기본적으로 _config.yml 에 전역적으로 permalink 값을 설정해주는게 맞다고 생각했는데 도저히 그 방법으로는 어떻게 조합을 해서 설정값을 적용해도 내가 원하는 결과가 나오지 않아서… 내 블로그 글마다 노가다식으로 permalink를 하나씩 전부 다 따로 설정하는 방법으로 문제를 일단 일단락 시켰다. 다행히 얼추 잘 되는거 같아서 내심 뿌듯하기는 하다. 하나씩 커스텀 해가는 맛이 있다는게 참 깃허브 블로그의 매력인거 같다. 물론 아직도 고쳐 나가야 할게 좀 남아있다. 왜 아직도 눈에 거슬리는게 보이는가.
(두눈 뜨고 보다보면 자꾸 보인다. 머리가 너무 팽팽 잘돌아가서 그런가?)
하여튼.. .html 확장자 문제는 해결이 되었고, 꾸준히 노력중이던 백준 골드가기 프로젝트. 이번년도 안에 꼭 이루고자 하는 부분인데. 이게 사실 그냥 티어적인 레벨만 올리는건 의미가 없고 내 힘으로 스스로 실력으로 문제를 풀고 올라가는거에 초점을 두고 있는데 정말이지 실버 문제들 치곤 내가 못풀겠다 싶은 문제들이 생각보다 많다. 최근에는 스택, 큐 Chapter에서 문제 내용 자체가 이해가 안가는 문제도 있어서 한문제만 남겨두고 해당 Chapter 마무리를 못하고 있는것도 있다. 그거 마무리하고싶은데 다른 사람 답을 보고 제출하고 문제풀이하고 완료했다고 눈가리고 아웅 하고 싶진 않달까. 내 힘으로 풀어야 골드 달성해도 떳떳할거 같다. 막상 문제 다른사람풀이보고 풀고 나서 제출한다음에 레벨올리면 롤로 따지만 대리로 랭크 올린거 같은 느낌이 들어서 너무 창피할거 같다.
(진짜 그건 내 기준에선 너무 구린거같다.)
여튼 그래서 현재 지금 실버1까지 어제 저녁 10-11시 사이에 달성을 했고. 이제는 한단계만 올라가면 골드인데 내가 앞으로 풀어야하는 문제들을 보니까 백트래킹, 재귀함수, 동적계획법, 그리디 알고리즘 …… 등 이제는 정말 알고리즘 공부가 필요한 부분의 문제들만 남아있다. 이제부터가 진짜구나 싶을정도랄까. 그래 이정도는 풀어야지 어디가서 알고리즘 풀어봤다고 당당하게 말할 수 있겠지?.
(와 나 이런거 무서워 하네? ㅎㅎ? ㅋㅋ? 그래도 쫄지는 말자.)
실버 1달성 기념 사진이나 하나 기록용으로 남겨둬야겠다. 나름 기록적인게. 백준시작한지 얼마되지 않았는데 시작일자를 확인해보니까 11월 4일부터 시작해서 오늘 일자로 11월 27일 이니까 거의 3주만에 브론즈에서 실버1까지 올라온거다 이정도면 정말 엄청난 속도 아닌가? 스스로 좀 뿌듯한걸?. 그래도 나쁘지않은 기본적인 문제풀이 실력은 가지고 있다고 볼수 있는게 아닐까?.
(진짜 짧은 기간이다. 나도 이정도로 짧은 기간이 걸린건지는 몰랐네?)
그럼에도 불구하고 조금 아쉬운점은 레벨업에만 집중하고 게시글의 질에 대해서는 생각하지 못해서. 최근에 작성된 알고리즘 문제들은 내가 문제를 풀고 코드에 주석도 달고, 문제해석? 문제를 어떻게 방향을 잡고 해석했고 로직을 구현했는지 작성을 해뒀는데, 이전 게시글은 그냥 정답 코드만 떡하니 있어서 뭔가 의미없는 게시글인거 같다. 나중에 한동안은 시간들여서 이전에 푼 문제들중에 문제해석이 없는글들은 전부 수정을 해야할거 같다. 엄청 귀찮은 일들이 생겨버렸다 젠장.
(아웅 귀찮앙.)
갑자기 좀 다른 이야기를 하고 싶은데 그건바로 이번주 블랙 프라이데이. 큰 결제를 큰맘먹고 지를수 있는날이기도 하다. 현재 내가 사용하고 있는 모니터는 삼성 스마트 모니터 M7을 쓰고 있는데 생각보다 눈이 좀 아픈부분도 있고 뭔가 VA 패널 때문인지 살짝 흐릿? 한 느낌이 있는거 같다. 이번기회에 LG 모니터로 바꾸고 싶은데 마땅한 제품이 있는지 찾아보고 있다. 눈에 여겨보고 있는건 삼성, 엘지, 델 브랜드 중에 하나인데 삼성은 이제는 좀 아닌거같다. 그럼 엘지랑 델인데 델은 가격이 생각보다 넘사더라고? 괜찮은 모니터가 100? 요 아래 이미지 사진 모니터가 유명한 4k 모니터 라인업 이더라. 비싸기도하고 맥북 모니터로 유명하기도 한거 같다. 근데 내가 가질수 없는 가격대의 모니터네. 눈물😭
(모니터 하나에 100만원을 태워? 난 아직 그정도의 재력가가 아니란 말이야.)
그렇다면 나에겐 LG 뿐이야… LG도근데 가격이 만만치 않다. 지금 찾아보고 있는건 LG 울트라HD 32UQ850V 제품인데 이정도 라인업도 내가보기엔 나한테는 좀 과한건 없지않아 있는데 이전에 사이드 모니터로 msi 10만원대 24인치 모니터를 세로로 돌려서 사용하고 있는데 이거 원.. FHD다 보니까 화질 다깨지고 눈아프고 난리도 아니다. 이제는 저 모니터 안보게 되는거같다. 내가 이런 경험을 통해 얻은 교훈은 모니터는 한번살때 진짜 괜찮은 제품을 사야한다는거다. 가성비 생각하다보면 나처럼 결국 두번구매한다. 이번에는 제대로 알아보고 두번구매안해야지 ㅠ 근데 또 욕심나는건 이제는 받침대로 모니터 쓰고싶지않고 자유롭게 암 설치해서 자유각도로 쓰고싶다. 사람의 욕심은 계속해서 끝이없는거 같다. 이번주안에 결정하고 구매해야하는데 오늘부터 틈틈히 특가 상품들 라인업들 눈여겨서 보고있어야 겠다. 모니터 꼭 구매할거야 내 눈을 위해서!
(모니터는 LG지 암 그렇고 말고.)
모니터 이야기로 글 마무리 하려다가 무언가 잊어버린게 있는거 같아서 다시 글 읽어보니까, 실버1 사진을 안올렸네? 이걸로 오늘의 글 여기까지 급 마무리!
(실버1 달성!)
-
[baekjoon] 통계학 (백준 2108 java 풀이)
intro : 최빈값을 구하는 부분을 염두해야 한다.
백준 문제링크
문제
수를 처리하는 것은 통계학에서 상당히 중요한 일이다. 통계학에서 N개의 수를 대표하는 기본 통계값에는 다음과 같은 것들이 있다. 단, N은 홀수라고 가정하자. 산술평균 : N개의 수들의 합을 N으로 나눈 값, 중앙값 : N개의 수들을 증가하는 순서로 나열했을 경우 그 중앙에 위치하는 값, 최빈값 : N개의 수들 중 가장 많이 나타나는 값, 범위 : N개의 수들 중 최댓값과 최솟값의 차이 N개의 수가 주어졌을 때, 네 가지 기본 통계값을 구하는 프로그램을 작성하시오.
입력
첫째 줄에 수의 개수 N(1 ≤ N ≤ 500,000)이 주어진다. 단, N은 홀수이다. 그 다음 N개의 줄에는 정수들이 주어진다. 입력되는 정수의 절댓값은 4,000을 넘지 않는다.
출력
첫째 줄에는 산술평균을 출력한다. 소수점 이하 첫째 자리에서 반올림한 값을 출력한다. 둘째 줄에는 중앙값을 출력한다. 셋째 줄에는 최빈값을 출력한다. 여러 개 있을 때에는 최빈값 중 두 번째로 작은 값을 출력한다. 넷째 줄에는 범위를 출력한다.
문제 풀이1 (배열 풀이 552ms)
import java.io.*;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter, StringBuilder 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
// 입력값의 개수
int count = Integer.parseInt(br.readLine());
// 평균값, 중앙값, 범위 값을 구하기 위해 사용되는 변수
int[] array = new int[count];
// 최빈값을 구하기 위해 사용되는 변수, -4000 값이 0번 인덱스 4000 값이 8000번 인덱스
int[] freq = new int[8001];
// 최빈값을 1차적으로 구하기 위한 변수
int maxFreq = 0;
// 합계를 구하기 위한 변수
int sum = 0;
// 반복문을 통해 합계 sum, array, freq (최빈값을 구하기 위한 배열 변수)
for (int i = 0; i < count; i++) {
int input = Integer.parseInt(br.readLine());
sum += input;
array[i] = input;
// 여기서 입력되는 값에 대해 각 인덱스에 맞게 값이 증가함
// 예를들어서 4000 값이 두번 입력되었다면 freq[8000] = 2
freq[input + 4000]++;
// 가장 많이 나온 최빈값을 찾기위해서 여기서 먼저 계산해서 값을 가지고 있음
// 1차로 먼저 값을 구하는 이유는, 최빈값이 동일한 값이 존재할 수 있기에 먼저 계산을 진행
maxFreq = Math.max(maxFreq, freq[input + 4000]);
}
// 배열 정렬 (중앙값을 찾기 위함에 있음)
Arrays.sort(array);
// 산술평균 값
sb.append(Math.round((double) sum / count)).append("\n");
// 중앙값
sb.append(array[count / 2]).append("\n");
// 최빈값 구하는 중요 로직
int realFreqValue = 0;
// 첫번째 최빈값을 찾을때 true 로 변경
boolean flag = false;
for (int i = 0; i < 8001; i++) {
if (maxFreq == freq[i]) {
if (!flag) {
// -4000을 하는 이유는 0번 인덱스에 -4000값을 할당하도록 위에서 설정하였기 때문
// 실제 값을 계산하기 위해서는 0번인덱스의 값 = i - 4000
realFreqValue = i - 4000;
flag = true;
} else {
// flag 가 ture 라는것은 이미 첫번째 최빈값을 찾았고 현재 이 로직으로 들어왔을때가
// 두번째로 작은 최빈값이라는 뜻
realFreqValue = i - 4000;
break;
}
}
}
// 최빈값 계산
sb.append(realFreqValue).append("\n");
// 범위값 계산
sb.append(array[count - 1] - array[0]).append("\n");
// bw로 출력하기 위해 문자열로 변환 후 write
bw.write(sb.toString());
bw.flush();
// 자원 반납
bw.close();
br.close();
}
}
문제 해석
이 문제는 마치 4개의 조각난 문제를 하나로 합친? 듯한 느낌이 드는 문제이다. 결국 구해야 하는 값을 정리해보자면 산술평균값, 중앙값, 최빈값, 범위 총 4가지인데 이중 최빈값을 제외한 나머지 값들은 하나의 배열에 값을 순차적으로 입력받고 계산하면 쉽게 풀리는데 이 최빈값 부분이 조건이 까다로웠다. 문제에서 예외적으로 최빈값이 여러개 있을 때는 두번째로 작은 값을 출력하라고 하였는데, 난 처음에는 이 문장을 읽고 오름차순으로 정렬한 배열에서 마지막 인덱스에서 - 1한 값이 두번째로 작은값을 뜻하는 줄 알았다. 뭔가 어떻게 읽냐에 따라서 문제의 해석이 좀 다르게 되는거 같은데 나에게만 해당하는 기분탓 일수도 있다…. 그래서 처음으로 풀었을때는 해당 문제에 대해서 틀렸었다.
산술평균값, 중앙값, 범위는 문제에서 주어진대로 계산을 하면 되는데, 최빈값 만큼은 로직구현이 다들 다르게 구성될거라 생각되어서 해당 부분을 나의 방법에 대해 설명하도록 하겠다. 나는 최빈값을 구하기 위해서 사실 처음에는 배열을 사용해서 문제 계산을 한게 아니라, 다양한 자료구조를 통해 문제를 해결하고자 하였다. 내가 처음으로 정답을 받은 코드를 먼저 확인해보자. (배열 사용 X, 다양한 Collection 자료구조 사용함)
문제 풀이2 (컬렉션 풀이 624ms)
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter, StringBuilder 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
// 입력값의 개수
int count = Integer.parseInt(br.readLine());
// 평균값, 중앙값, 범위 값을 구하기 위해 사용되는 변수
int[] array = new int[count];
// 최빈값을 구하기 위해 사용되는 변수
Map<Integer, Integer> map = new HashMap<>(); // 최빈값 구하기
// 합계를 구하기 위한 변수
int sum = 0;
for (int i = 0; i < count; i++) {
int input = Integer.parseInt(br.readLine());
sum += input;
array[i] = input;
// 최빈값을 찾기 위한 map 을 통한 값 저장
// key 값 = input 값, value 값 = 입력 횟수
map.put(input, map.getOrDefault(input, 0) + 1);
}
// 배열 정렬 (중앙값을 찾기 위함에 있음)
Arrays.sort(array);
// 산술평균 값
sb.append(Math.round((double) sum / count)).append("\n");
// 중앙값
sb.append(array[array.length / 2]).append("\n");
// 최빈값
sb.append(findMode(map)).append("\n");
// 범위
sb.append(array[array.length - 1] - array[0]).append("\n");
// bw로 출력하기 위해 문자열로 변환 후 write
bw.write(sb.toString());
bw.flush();
// 자원반납
bw.close();
br.close();
}
// 최빈값을 찾기 위한 메서드
public static int findMode(Map<Integer, Integer> map) {
// 가장 큰 값을 찾기 위한 변수 선언
int maxValue = Integer.MIN_VALUE;
// 반복문을 통해 value 값이 가장 큰 값 찾기
for (Integer key : map.keySet()) {
Integer value = map.get(key);
if (value > maxValue) {
maxValue = value;
}
}
// Set 자료구조중에 TreeSet은 자동으로 정렬해주는 특징을 가지고 있다.
Set<Integer> set = new TreeSet<>();
for (Map.Entry<Integer, Integer> es : map.entrySet()) {
// 가장 큰값에 해당하는 key 값들을 set에 할당
if (es.getValue() == maxValue) {
set.add(es.getKey());
}
}
// Set 자료구조를 List로 변환
List<Integer> list = new ArrayList<>(set);
if (list.size() > 1) {
// 최빈값이 여러개라면? 두번째로 작은 값 retrun
return list.get(1);
} else {
// 최빈값이 하나라면 첫번째 값 return
return list.get(0);
}
}
}
왜 컬렉션 풀이에서 배열 풀이로 리팩토링을 하였는가?
위 방법으로 문제를 풀었을때는 최빈값부분에서 아무래도 형변환도 자주 일어나고 findMode 메서드를 통해 내부적으로 반복문이 한번더 실행되는게 시간복잡도 관점에서 보았을때 좀 별로라고 생각하였고, 최빈값을 구하는데에 있어서 저렇게 코드가 번잡하게 작성해야 할 필요가 있는가? 라는 물음에 스스로 답을 해보았을때 리팩토링이 필요하다고 생각이 되었다. 해서 문제의 조건을 다시 읽어보니, 입력되는 값의 범위가 절대값 4000까지 라는 문장이 숨어있었고, 해당 문장을 읽고나서 배열을 통해서 최빈값을 찾을 수 있겠다는 생각이 들어서 문제풀이1 과 같은 코드를 재 작성하게 되었다.
배열을 사용할수 있었던 이유
입력값이 범위가 절대값 4000 과 같이 문제에서 주어지지 않는다면 배열을 통해서 어느정도의 배열크기를 지정해서 선언하고 최빈값을 기록해야 하는지 알 수 없기에 컬렉션을 통해 최빈값을 찾는게 좋은 방법이 될 수 있지만, 현재 문제에서는 정확히 입력값의 범위가 주어졌기 때문에 배열을 통해서 푸는게 출제자가 의도한 문제 해답에 좀 더 가까운 해결책이라고 생각이 들었다.
배열풀이에서 가장 어려웠던 부분
배열을 통해서 본격적으로 코드를 작성할때 가장 난관이었던 부분은 최빈값이 여러개일때 두번째로 작은 값을 어떻게 구할것인가? 잘 생각해보니 freq라는 배열은 이미 어떻게 보면 정렬이 되어있는 배열이다. 그말이 무슨말이냐면 배열의 크기가 8001이며 0번 인덱스부터 8000번 인덱스 까지 값이 순차적으로 할당되어 있다. 이말은 정렬이 되어있는 최빈값의 배열이라는 말과 동일하다.
예를들어서 최빈값의 크기가 8이고 freq[0] = 1, freq[1] = 2, …. freq[302] = 8, …… freq[3302] = 8, …. 이런식의 상황이 있다면 순차적으로 freq 배열을 반복문을 돌았을때 첫번째 최빈값을 찾는 인덱스는 302번이다. 다만 최빈값이 8인 인덱스가 3302에도 있기에 두번째로 작은 값은 3302번이 되는데, 여기서 왜 이값이 두번쨰로 작은 값이냐면 위에서 freq 배열에 값을 할당할때 + 4000된 값을 freq 배열에 할당하였기에 실제로 return 되는 값은 3302 - 4000 값으로 -698이 return 된다.
마무리
위와 같은 방법을 통해 배열과 컬렉션을 사용한 문제풀이를 작성할 수 있었고, 어떤 상황에서 배열을 적용할지 컬렉션을 적용할지에 대해서도 느끼는 점이 많은 문제였던거 같다.
-
[java] 메소드
intro : java 메소드에 대한 개념을 알아보자.
메서드 시작
두 숫자를 입력 받아서 더하고 출력하는 단순한 기능을 개발해보자. 먼저 1 + 2 를 수행하고, 그 다음으로 10 + 20 을 수행할 것인데, 아래 코드를 보면 두개의 변수를 받아서 합을 구하는 연산 과정이 중복이 된다. 이 중복되는 부분 어떻게 개선을 하면 좋을까 ?
public class Method1 {
public static void main(String[] args) {
// 계산1
int a = 1;
int b = 2;
System.out.println(a + "+" + b + " 연산 수행");
int sum1 = a + b;
System.out.println("결과1 출력:" + sum1);
// 계산2
int x = 10;
int y = 20;
System.out.println(x + "+" + y + " 연산 수행");
int sum2 = x + y;
System.out.println("결과2 출력:" + sum2);
}
}
메서드 사용
자바에서는 함수를 메서드(Method)라 한다. 메서드도 함수의 한 종류라고 생각하면 된다. 지금은 둘을 구분하지 않고, 이정도만 알아두자. 앞서 작성한 코드의 중복된 부분을 메서드를 이용하여 개선해보자.
public class Method1Ref {
public static void main(String[] args) {
int sum1 = add(5, 10);
System.out.println("결과1 출력:" + sum1);
int sum2 = add(15, 20);
System.out.println("결과2 출력:" + sum2);
}
// add 메서드
public static int add(int a, int b) {
System.out.println(a + "+" + b + " 연산 수행");
int sum = a + b;
return sum;
}
}
아래와 같은 코드에 add 함수를 메서드라고 한다. 메서드의 역할은 어떤 연산을 처리한 다음에 결과를 반환한다. (무조건적인 반환을 해주어야 하는건 아니다.)
public static int add(int a, int b) {
System.out.println(a + "+" + b + " 연산 수행");
int sum = a + b;
return sum;
}
메서드의 구조
메서드의 구조는 크게 2가지로 나눌 수 있는데, 메서드 선언부와 본문으로 나눌 수 있다.
메서드 선언
메서드 이름 반환타입 매개변수 목록을 포함한다. 이름 그대로 이런 메서드가 있다고 선언하는 것이다. 메서드 선언 정보를 통해 다른 곳에서 해당 메서드를 호출할 수 있다.
// add 메서드 선언부
public static int add(int a, int b)
메서드 본문
메서드가 수행해야 하는 코드 블록이다. 메서드를 호출하면 메서드 본문이 순서대로 실행된다. 메서드 본문은 블랙박스이다. 메서드를 호출하는 곳에서는 메서드 선언은 알지만 메서드 본문은 모른다. 메서드의 실행 결과를 반환하려면 return 문을 사용해야 한다. return 문 다음에 반환할 결과를 적어주면 된다.
{
// add 메서드 본문
System.out.println(a + "+" + b + " 연산 수행");
int sum = a + b;
return sum;
}
메서드 호출과 용어정리
메서드를 호출할 때는 다음과 같이 메서드에 넘기는 값과 매개변수(파라미터)의 타입이 맞아야 한다. 물론 넘기는 값과 매개변수(파라미터)의 순서와 갯수도 맞아야 한다.
인수 (Argument)
여기서 "hello" , 20 처럼 메서드에 넘기는 값을 영어로 Argument(아규먼트), 한글로 인수 또는 인자라 한다.실무에서는 아규먼트, 인수, 인자라는 용어를 모두 사용한다.
매개변수(Parameter)
메서드를 정의할 때 선언한 변수인 String str , int age 를 매개변수, 파라미터라 한다. 메서드를 호출할 때 인수를 넘기면, 그 인수가 매개변수에 대입된다. 실무에서는 매개변수, 파라미터 용어를 모두 사용한다.
위 둘의 단어는 반드시 분리되어 사용되어야 한다 혼용되어선 안된다.
메서드 정의
메서드는 다음과 같이 정의 할 수 있다.
public static int add(int a, int b) {
// 메서드 본문, 실행 코드
}
제어자 반환타입 메서드이름(매개변수 목록) {
메서드 본문
}
제어자(Modifier)
public , static 과 같은 부분이다. 제어자는 뒤에서 설명한다. 지금은 항상 public static 키워드를 입력하자.
반환 타입(Return Type)
메서드가 실행 된 후 반환하는 데이터의 타입을 지정한다. 메서드가 값을 반환하지 않는 경우, 없다는 뜻의 void 를 사용해야 한다. 예) void print(String str)
메서드 이름(Method Name)
메서드의 이름이다. 이 이름은 메서드를 호출하는 데 사용된다.
매개변수(Parameter)
입력 값으로, 메서드 내부에서 사용할 수 있는 변수이다. 매개변수는 옵션이다. 입력값이 필요 없는 메서드는 매개변수를 지정하지 않아도 된다. 예) add()
메서드 본문(Method Body)
실제 메서드의 코드가 위치한다. 중괄호 {} 사이에 코드를 작성한다.
반환 타입
반환 타입이 있으면 반드시 값을 반환해야 한다. 반환 타입이 있는 메서드는 반드시 return 을 사용해서 값을 반환해야 한다.
아래 코드에서 if 조건이 만족할 때는 true 가 반환되지만, 조건을 만족하지 않으면 어떻게 될까? 조건을 만족하지 않은 경우에는 return 문이 실행되지 않는다. 따라서 이 코드를 실행하면 return 문을 누락했다는 다음과 같은 컴파일 오류가 발생한다.
public class MethodReturn1 {
public static void main(String[] args) {
boolean result = odd(2);
System.out.println(result);
}
public static boolean odd(int i) {
if (i % 2 == 1) {
return true;
}
// 반드시 boolean 타입으로 return이 되어야 하는 메서드 인데 if문 조건을 만족하지 못한다면
// return 되지 않기에 컴파일 오류가 발생한다.
}
}
위 코드는 다음과 같이 수정되어야 한다.
public class MethodReturn1 {
public static void main(String[] args) {
boolean result = odd(2);
System.out.println(result);
}
public static boolean odd(int i) {
if (i % 2 == 1) {
return true;
} else {
// 조건문을 만족하지 않는다면 false 반환
return false;
}
}
}
return 문을 만나면 그 즉시 메서드를 빠져나간다.
return 문을 만나면 그 즉시 해당 메서드를 빠져나가는데 다음과 같이 활용할 수 있다. (메서드를 특정 시점에 강제로 종료해야할때 자주 사용한다.)
public class MethodReturn2 {
public static void main(String[] args) {
checkAge(10);
checkAge(20);
}
public static void checkAge(int age) {
if (age < 18) {
System.out.println(age + "살, 미성년자는 출입이 불가능합니다.");
// return 문을 만나면 메서드가 종료되고, 아래 출력문이 실행되지 않는다.
return;
}
System.out.println(age + "살, 입장하세요.");
}
}
메서드 호출과 값 전달1
자바에서 중요한 원칙이 있는데, 자바는 값을 대입할때 변수의 값을 복사해서 대입한다. 아래 코드를 통해서 내가 이해한 중요한 원칙을 제대로 이해했는지 확인해보자. 아래 주석으로 출력 결과를 작성했다. 혹시 틀리게 생각한 부분이 있다면 변수의 값을 복사해서 대입한다는 개념이 아직 부족하다는 뜻이다.
public class MethodValue1 {
public static void main(String[] args) {
int num1 = 5;
System.out.println("1. changeNumber 호출 전, num1: " + num1);
changeNumber(num1);
System.out.println("4. changeNumber 호출 후, num1: " + num1);
}
public static void changeNumber(int num2) {
System.out.println("2. changeNumber 변경 전, num2: " + num2);
num2 = num2 * 2;
System.out.println("3. changeNumber 변경 후, num2: " + num2);
}
}
// 1. changeNumber 호출 전, num1: 5
// 2. changeNumber 변경 전, num2: 5
// 3. changeNumber 변경 후, num2: 10
// 4. changeNumber 호출 후, num1: 5
메서드 호출과 값 전달2
다시한번 중요한 원칙인 자바는 값을 복사해서 대입한다라는 개념을 이해했는지 재확인해보자.
public class MethodValue2 {
public static void main(String[] args) {
int number = 5;
System.out.println("1. changeNumber 호출 전, number: " + number); // 출력: 5
changeNumber(number);
System.out.println("4. changeNumber 호출 후, number: " + number); // 출력: 5
}
public static void changeNumber(int number) {
System.out.println("2. changeNumber 변경 전, number: " + number); // 출력: 5
number = number * 2;
System.out.println("3. changeNumber 변경 후, number: " + number); // 출력: 10
}
}
// 1. changeNumber 호출 전, number: 5
// 2. changeNumber 변경 전, number: 5
// 3. changeNumber 변경 후, number: 10
// 4. changeNumber 호출 후, number: 5
메서드 호출과 값 반환받기
메서드를 사용해서 값을 변경하려면 어떻게 해야할까? 메서드의 호출 결과를 반환 받아서 사용하면 된다.
public class MethodValue3 {
public static void main(String[] args) {
int num1 = 5;
System.out.println("changeNumber 호출 전, num1: " + num1); // 출력: 5
num1 = changeNumber(num1);
System.out.println("changeNumber 호출 후, num1: " + num1); // 출력: 10
}
public static int changeNumber(int num2) {
num2 = num2 * 2;
return num2;
}
}
메서드와 형변환
메서드를 호출할 때도 형변환이 적용된다. 메서드 호출과 명시적 형변환, 자동 형변환에 대해 알아보자.
명시적 형변환
메서드를 호출하는데 인자와 매개변수의 타입이 맞지 않다면 어떻게 해야할까? 아래 주석을 풀고 실행하면 다음과 같은 오류가 발생한다. (java: incompatible types: possible lossy conversion from double to int)
public class MethodCasting1 {
public static void main(String[] args) {
double number = 1.5;
// double을 int형에 대입하므로 컴파일 오류
// 반드시 실행하고 싶다면 (int) number 을 인수로 전달해야 한다.
//printNumber(number);
printNumber((int) number); // 명시적 형변환을 사용해 double을 int로 변환
}
public static void printNumber(int n) {
System.out.println("숫자: " + n);
}
}
자동 형변환
int < long < double 메서드를 호출할 때 매개변수에 값을 전달하는 것도 결국 변수에 값을 대입하는 것이다. 따라서 앞서 배운 자동 형변환이 그대로 적용된다. 메서드를 호출할 때는 전달하는 인수의 타입과 매개변수의 타입이 맞아야 한다. 단 타입이 달라도 자동 형변환이 가능한 경우에는 호출할 수 있다.
byte → short → int → long → float → double
char → int → long → float → double
public class MethodCasting2 {
public static void main(String[] args) {
int number = 100;
printNumber(number); // int에서 double로 자동 형변환
}
public static void printNumber(double n) {
System.out.println("숫자: " + n);
}
}
메서드 오버로딩
자바는 메서드의 이름 뿐만 아니라 매개변수 정보를 함께 사용해서 메서드를 구분한다. 따라서 다음과 같이 이름이 같고, 매개변수가 다른 메서드를 정의할 수 있다. 이렇게 이름이 같고 매개변수가 다른 메서드를 여러개 정의하는 것을 메서드 오버로딩(Overloading)이라 한다. 오버로딩은 번역하면 과적인데, 과하게 물건을 담았다는 뜻이다. 따라서 같은 이름의 메서드를 여러개 정의했다고 이해하면 된다.
오버로딩 규칙
메서드의 이름이 같아도 매개변수의 타입 및 순서가 다르면 오버로딩을 할 수 있다. 참고로 반환 타입은 인정하지 않는다.
메서드 시그니처(method signature)
메서드 시그니처 = 메서드 이름 + 매개변수 타입(순서)
메서드 시그니처는 자바에서 메서드를 구분할 수 있는 고유한 식별자나 서명을 뜻한다. 메서드 시그니처는 메서드의 이름과 매개변수 타입(순서 포함)으로 구성되어 있다. 쉽게 이야기해서 메서드를 구분할 수 있는 기준이다. 자바 입장에서는 각각의 메서드를 고유하게 구분할 수 있어야한다. 그래야 어떤 메서드를 호출 할 지 결정할 수 있다. 따라서 메서드 오버로딩에서 설명한 것 처럼 메서드 이름이 같아도 메서드 시그니처가 다르면 다른 메서드로 간주한다. 반환 타입은 시그니처에 포함되지 않는다.
다음 코드는 오버로딩을 활용한 코드이다.
public class Overloading1 {
public static void main(String[] args) {
System.out.println("1: " + add(1, 2));
System.out.println("2: " + add(1, 2, 3));
}
// 첫 번째 add 메서드: 두 정수를 받아서 합을 반환한다.
public static int add(int a, int b) {
System.out.println("1번 호출");
return a + b;
}
// 두 번째 add 메서드: 세 정수를 받아서 합을 반환한다.
// 첫 번째 메서드와 이름은 같지만, 매개변수 목록이 다르다.
public static int add(int a, int b, int c) {
System.out.println("2번 호출");
return a + b + c;
}
}
아래코도는 오버로딩 케이스중에 헷갈릴만한 케이스인데, add 메서드는 파라미터가 실수형 타입 double을 인자값으로 받고있다. 이때 add(1, 2)를 호출하게 된다면 어떻게 될까? 단순히 실수형 타입에 정수형 타입을 넣어야 하니 오류가발생할것 같지만 실제로는 자동형변환이 일어나서 add(1, 2) 가 정상적으로 실행된다.
public class Main {
public static void main(String[] args) {
System.out.println("1: " + add(1, 2));
System.out.println("2: " + add(1.2, 1.5));
}
public static double add(double a, double b) {
System.out.println("add 메서드 호출");
return a + b;
}
}
// add 메서드 호출
// 1: 3.0
// add 메서드 호출
// 2: 2.7
-
[java] 배열
intro : java 배열에 대한 개념을 알아보자.
배열 시작
배열이 필요한 이유
만약 학생의 점수를 출력해야하는 간단한 프로그램을 구성한다고 생각해보자. 각 학생이 추가될때마다 새로운 변수에 점수를 담고, 해당 점수를 출력을 하게되는데, 아직 배열에 대한 개념을 배우기 전이기 때문에 다음과 같은 코드를 통해 조금 힘들게 구성을 해야한다. 이렇게 같은 타입의 변수를 반복해서 선언하고 반복해서 사용하는 문제를 해결할수 있는게 배열이라는 개념이다.
public class Array1 {
public static void main(String[] args) {
int student1 = 90;
int student2 = 80;
int student3 = 70;
int student4 = 60;
int student5 = 50;
System.out.println("학생1 점수: " + student1);
System.out.println("학생2 점수: " + student2);
System.out.println("학생3 점수: " + student3);
System.out.println("학생4 점수: " + student4);
System.out.println("학생5 점수: " + student5);
}
}
배열의 선언과 생성
배열은 같은 타입의 변수를 사용하기 편리하게 하나로 묶어둔 것이다. 위 예제에서는 학생이라는 변수는 같은 int형 타입으로 묶을수 있는데 다음과 같이 코드를 구성할 수 있다.
public class Array1Ref1 {
public static void main(String[] args) {
int[] students; // 배열 변수 선언
students = new int[5]; // 배열 생성
// 변수 값 대입
students[0] = 90;
students[1] = 80;
students[2] = 70;
students[3] = 60;
students[4] = 50;
// 변수 값 사용
System.out.println("학생1 점수: " + students[0]);
System.out.println("학생2 점수: " + students[1]);
System.out.println("학생3 점수: " + students[2]);
System.out.println("학생4 점수: " + students[3]);
System.out.println("학생5 점수: " + students[4]);
}
}
배열의 생성 단계
배열의 생성 단계에는 다음과 같은 단계를 따르는데 배열의 선언, 배열 생성, 배열 참조값 보관을 따른다. 배열의 선언은 int[] students; 이처럼 내가 공통으로 묶고자 하는 타입을 [ ] 대괄호를 이용하여 묶고 변수를 선언하는 것을 말한다. 이후 해당 변수의 실제 생성 부분은 new 연산자를 이용해 new int[5] 배열의 사이즈를 지정한다. 이렇게 배열을 선언하고 생성하게되면 선언한 변수에 생성한 배열 참조값을 보관하게 된다. 참조값이란? 기존 변수 Chapter에서 배운 내용은 기본형 변수 타입 인데, 기본형 변수 타입을 제외한 모든 변수 타입은 참조형 변수 타입으로 주소값(참조값)을 통해 접근할 수 있다.
배열 사용
인덱스
배열은 변수와 사용법이 비슷한데, 차이점이 있다면 다음과 같이 [] 사이에 숫자번호를 넣어주면 된다. 배열의 위치를 나타내는 것을 index(인덱스) 라고 한다. 참고로 배열에서 인덱스는 1이 아니라 0 부터시작한다. 만약 접근 가능한 배열의 인덱스 범위를 넘어가면 java.lang.ArrayIndexOutOfBoundsException 오류가 발생한다.
// 변수 값 대입
students[0] = 90;
students[1] = 80;
// 변수 값 사용
System.out.println("학생1 점수: " + students[0]);
System.out.println("학생2 점수: " + students[1]);
배열에 값 대입
배열에 값을 대입하던지 배열의 값을 사용하던지 일반적인 변수와 사용법은 같다. 추가로 [] 를 통해 접근하고자 하는 배열의 인덱스 값만 넣어주면 된다.
students[0] = 90; // 1. 배열에 값을 대입
x001[0] = 90; // 2. 변수에 있는 참조값을 통해 실제 배열에 접근. 인덱스를 사용해서 해당 위치의 요소에 접근, 값 대입
students[1] = 80; // 1. 배열에 값을 대입
x001[1] = 80; // 2. 변수에 있는 참조값을 통해 실제 배열에 접근. 인덱스를 사용해서 해당 위치의 요소에 접근, 값 대입
배열 값 읽기
배열을 사용할때는 다음과 같이 참조값을 통해서 실제 배열에 접근하고 인덱스를 통해서 원하는 요소를 찾을 수 있다.
// 1. 변수 값 읽기
System.out.println("학생1 점수: " + students[0]);
// 2. 변수에 있는 참조값을 통해 실제 배열에 접근. 인덱스를 사용해서 해당 위치의 요소에 접근
System.out.println("학생1 점수: " + x001[0]); // students 변수의 주소값이 x001 가정
// 3. 배열의 값을 읽어옴
System.out.println("학생1 점수: " + 90);
기본형 vs 참조형
자바의 변수 데이터 타입을 가장 크게 보면 기본형과 참조형으로 분류할 수 있다. 사용하는 값을 직접 넣을 수 있는 기본
형, 그리고 방금 본 배열 변수와 같이 메모리의 참조값을 넣을 수 있는 참조형으로 분류할 수 있다. 기본형(Primitive Type): 우리가 지금까지 봤던 int , long , double , boolean 처럼 변수에 사용할 값을 직접 넣을 수 있는 데이터 타입을 기본형(Primitive Type)이라 한다. 참조형(Reference Type): int[] students 와 같이 데이터에 접근하기 위한 참조(주소)를 저장하는 데이터 타입을 참조형(Reference Type)이라 한다. (위에서 살짝 언급한 내용을 다시 한번 정리한다.)
배열은 왜 참조형을 사용할까 ?
int, long, double 등 기본형 타입 변수들은 선언과 동시에 사이즈가 정해진다. int (4 Byte), long(8 Byte), double(8 Byte) ….. 배열은 생성시점에 동적으로 크기를 지정하게 되는데 예를들면 int[] array = new int[10] 인 경우는 배열의 사이즈가 int 타입이 10개 있기에 40 Byte가 할당된다. 이렇게 참조형 타입을 사용하면 동적으로 크기를 설정할 수 있는 유연성을 제공 할 수 있고 복잡한 데이터 구조도 만들고 관리할 수 있는 장점이 생기기에 배열은 참조형을 사용한다.
배열 리펙토링
아래 코드를 보면 배열의 선언과 생성 부분에서 나왔던 코드랑은 굉장히 비교가 될 정도로 짧아지고 효율적인 코드를 작성할 수 잇게된다. 학생이 추가 될때마다 변수를 새로 생성하고, 출력문을 다시 작성했던것에 비해 아래 코드는 만약 학생이 추가될 경우 students 변수에 값을 하나 더 추가해주면 반복문을 통해 자동으로 새로 추가된 학생의 값 또한 출력 할 수 있게 된다.
public class Array1Ref4 {
public static void main(String[] args) {
//배열 생성 간략 버전, 배열 선언과 함께 사용시 new int[] 생략 가능
int[] students = {90, 80, 70, 60, 50};
for (int i = 0; i < students.length; i++) {
System.out.println("학생" + (i + 1) + " 점수: " + students[i]);
}
}
}
2차원 배열 - 시작
지금까지 학습한 배열은 단순히 순서대로 나열되어 있었다. 이것을 1차원 배열이라 한다. 이번에 학습할 2차원 배열은 이름 그대로 하나의 차원이 추가된다. 2차원 배열은 행과 열로 구성된다. 2차원 배열의 사용법은 [] 가 하나 추가되는 것을 제외하고는 앞서본 1차원 배열과 같다. 아래 코드를 통해 2차원 배열에 대해서 알아보자.
public class ArrayDi0 {
public static void main(String[] args) {
// 2x3 2차원 배열을 만든다.
int[][] arr = new int[2][3]; // 행(row), 열(column)
arr[0][0] = 1; // 0행, 0열
arr[0][1] = 2; // 0행, 1열
arr[0][2] = 3; // 0행, 2열
arr[1][0] = 4; // 1행, 0열
arr[1][1] = 5; // 1행, 1열
arr[1][2] = 6; // 1행, 2열
// 0행 출력
System.out.print(arr[0][0] + " "); // 0열 출력
System.out.print(arr[0][1] + " "); // 1열 출력
System.out.print(arr[0][2] + " "); // 2열 출력
System.out.println(); // 한 행이 끝나면 라인을 변경한다.
// 1행 출력
System.out.print(arr[1][0] + " "); // 0열 출력
System.out.print(arr[1][1] + " "); // 1열 출력
System.out.print(arr[1][2] + " "); // 2열 출력
System.out.println(); // 한 행이 끝나면 라인을 변경한다.
}
}
2차원 배열 - 리펙토링1
위 2차원 배열에서의 코드를 보면 중복된 부분이 존재한다. 0행과 1행을 출력하는 코드의 내용을 자세히보면 결국 2차원 배열에서의 행과 열에 대한 인덱스를 순차적으로 접근하여 출력하면 된다는 것을 눈치 챌 수 있다. 아래 코드처럼 2중 반복문을 통해 순차적으로 2차원 배열의 인덱스를 활용하여 코드를 깔끔하게 작성할 수 있다.
public class ArrayDi2 {
public static void main(String[] args) {
// 2x3 2차원 배열을 만듭니다.
int[][] arr = new int[2][3]; // 행(row), 열(column)
arr[0][0] = 1; // 0행, 0열
arr[0][1] = 2; // 0행, 1열
arr[0][2] = 3; // 0행, 2열
arr[1][0] = 4; // 1행, 0열
arr[1][1] = 5; // 1행, 1열
arr[1][2] = 6; // 1행, 2열
for (int row = 0; row < 2; row++) {
for (int column = 0; column < 3; column++) {
System.out.print(arr[row][column] + " ");
}
System.out.println(); // 한 행이 끝나면 라인을 변경한다.
}
}
}
2차원 배열 - 리펙토링2
리팩토링 1번에서의 부족한 부분을 조금더 찾아보면 배열의 선언과 초기화하는 부분에서 조금 아쉬운 부분이 존재한다, 2중 반복문에서 조건식에 해당하는 부분의 코드가 하드코딩으로 행과 열의 값 2, 3 으로 작성되어 있는게 아쉽다. 배열은 동적으로 크기가 변할 수 있기 때문에 변경 포인트를 최대한 줄이는게 유지보수 측면에서도 좋기에 위 문제를 리팩토링하여 개선한 코드는 다음과 같이 작성할 수 있다.
public class ArrayDi3 {
public static void main(String[] args) {
// 2x3 2차원 배열, 초기화
// 배열의 생성시 new int[2][3] 대신에 다음과 같이 {} 를 이용해 직접 작성할 수 있다.
int[][] arr = {
{1, 2, 3},
{4, 5, 6}
};
// 2차원 배열의 길이를 활용
// 행과 열의 크기를 배열의 length 메소드를 통해 지정
for (int row = 0; row < arr.length; row++) {
for (int column = 0; column < arr[row].length; column++) {
System.out.print(arr[row][column] + " ");
}
System.out.println();
}
}
}
향상된 for문 (배열같은 순차적인 데이터에 접근해야할때 유용하다)
보통 for-each 문으로 부르며 향상된 for문은 기존의 반복문 보다 더 편리하게 사용할 수 있다. 물론 모든 상황에서 편리하게 사용할 수 있는건 아니지만 대부분의 상황에서는 향상된 for문을 사용한다. (index 값을 사용해야 하는 경우는 향상된 for문이 아니라, 기존에 사용하던 for문을 사용하자)
향상된 for문 정의
for (변수 : 배열 또는 컬렉션) {
// 배열 또는 컬렉션의 요소를 순회하면서 수행할 작업
}
향상된 for문 예제
public class EnhancedFor1 {
public static void main(String[] args) {
int[] numbers = {1, 2, 3, 4, 5};
//일반 for문
for(int i = 0; i < numbers.length; ++i) {
int number = numbers[i];
System.out.println(number);
}
//향상된 for문, for-each문
for (int number : numbers) {
System.out.println(number);
}
//for-each문을 사용할 수 없는 경우, 증가하는 index 값 필요
for(int i = 0; i < numbers.length; ++i) {
System.out.println("number" + i + "번의 결과는: " + numbers[i]);
}
}
}
-
[design-pattern] template method 패턴
intro : template method 패턴에 대해서 알아보자
Template Method 패턴이란 ?
Template Method 패턴은 객체지향 설계에서 자주 사용하는 행동(Behavioral) 디자인 패턴 중 하나로, 상위 클래스에서 알고리즘의 구조를 정의하고, 하위 클래스에서 일부 세부적인 동작을 구체적으로 구현하도록 허용하는 패턴이다. 즉, 알고리즘의 골격을 정의하고 세부적인 내용은 서브클래스에서 완성되도록 유도한다.
Template Method 패턴은 언제 사용하는가 ?
Template Method 패턴은 알고리즘의 전체 구조(골격)는 고정하되, 특정 단계만 커스터마이징해야 할 때 사용됩니다. 이 패턴은 공통된 프로세스를 상위 클래스에 정의하고, 세부적인 동작을 하위 클래스에서 구현하도록 한다.
Template Method 패턴 적용 코드 (1)
골격을 정의할 Beverage 추상 클래스, 차후 Beverage를 구현할 하위 클래스가 세부 역할을 정의하게 된다.
abstract class Beverage {
// Template method (큰 틀과 골격, 이부분은 변하지 않는다.)
// 하위 클래스에서 재정의가 되지 않도록 final 키워드를 사용하였다.
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
void boilWater() {
System.out.println("Boiling water");
}
void pourInCup() {
System.out.println("Pouring into cup");
}
// 하위 클래스에서 정의할 메소드들
abstract void brew();
abstract void addCondiments();
}
Beverage 추상 클래스를 상속받아 구현한 Tea, Coffee 클래스이다. 해당 클래스들은 brew, addCondiments 메서드들을 각각 구현한다.
// 실제 구현체 클래스에서 추상 메서드 재정의
class Tea extends Beverage {
void brew() {
System.out.println("Steeping the tea");
}
void addCondiments() {
System.out.println("Adding lemon");
}
}
// 실제 구현체 클래스에서 추상 메서드 재정의
class Coffee extends Beverage {
void brew() {
System.out.println("Dripping coffee through filter");
}
void addCondiments() {
System.out.println("Adding sugar and milk");
}
}
클라이언트는 다음과 같이 Beverage 타입으로 Tea, Coffee 객체를 생성한다. (다형성의 특징) Beverage 클래스에 미리 정의된 prepareRecipe 메서드는 내부적으로 고유한 골격과 순서를 가지고 잇기 때문에 어떠한 구현체 클래스가 prepareRecipe 메서드를 호출하더라도 메서드 내부의 순서는 다르지 않고, brew, addCondiments 메서드의 Overriding 내용만 다르게 작동한다.
// Client code
public class Main {
public static void main(String[] args) {
Beverage tea = new Tea();
Beverage coffee = new Coffee();
System.out.println("\nMaking tea...");
tea.prepareRecipe();
System.out.println("\nMaking coffee...");
coffee.prepareRecipe();
}
}
Template Method 패턴 적용 코드 (2)
위 내용과 비슷한 상황이지만, Template Method가 분기에 따라 다르게 동작하는 상황에 대해서도 적용할 수 있다. (보다 실무에 가까운 예제)
// 해당 패턴이 인터페이스를 사용하지 않는 이유는
// 템플릿 메서드는 상위 클래스에서 구현이 되어 있어야 하기 때문.
// 추상클래스의 사용 이유가 된다.
abstract class DataProcessor {
// Template method (큰 틀과 골격, 이부분은 변하지 않는다.)
// 하위 클래스에서 재정의가 되지 않도록 final 키워드를 사용하였다.
public final void process(String data) {
loadData(data);
if (isValidData(data)) {
processData(data);
saveData(data);
} else {
System.out.println("Data is invalid, processing aborted.");
}
}
// 추상 메서드를 정의하고 하위 클래스에서 구현할수 있도록 정의
// protected 키워드는 자식 클래스에서는 접근가능한 접근 제어자
protected abstract void loadData(String data);
protected abstract boolean isValidData(String data);
protected abstract void processData(String data);
protected abstract void saveData(String data);
}
BeveraDataProcessorge 추상 클래스를 상속받아 구현한 CSVDataProcessor, JSONDataProcessor 클래스이다. 해당 클래스들은 loadData, isValidData, processData, saveData 메서드들을 각각 구현한다. 위 예제와 같은 역할이다 결국 상위 클래스에서 정의한 추상 메서드들을 각 클래스의 역할에 맞게 재정의 할 뿐이다.
// 실제 구현체 클래스에서 추상 메서드 재정의
class CSVDataProcessor extends DataProcessor {
@Override
protected void loadData(String data) {
System.out.println("Loading data from CSV file: " + data);
}
@Override
protected boolean isValidData(String data) {
return data != null && data.contains("CSV");
}
@Override
protected void processData(String data) {
System.out.println("Processing CSV data");
}
@Override
protected void saveData(String data) {
System.out.println("Saving CSV data to database");
}
}
// 실제 구현체 클래스에서 추상 메서드 재정의
class JSONDataProcessor extends DataProcessor {
@Override
protected void loadData(String data) {
System.out.println("Loading data from JSON file: " + data);
}
@Override
protected boolean isValidData(String data) {
return data != null;
}
@Override
protected void processData(String data) {
System.out.println("Processing JSON data");
}
@Override
protected void saveData(String data) {
System.out.println("Saving JSON data to database");
}
}
클라이언트 코드 즉 Main에서 DataProcessor 타입으로 CSVDataProcessor, JSONDataProcessor 객체들을 사용한다. 이때 실행하는 메서드 process 는 추상클래스인 DataProcessor의 템플릿 메서드 이므로, 상위클래스에서 정의된 순서대로 실행된다. (템플릿 메서드 안에 내부적으로 메서드가 순차적으로 정의 되어 있음)
// Client code
public class Main {
public static void main(String[] args) {
DataProcessor csvProcessor = new CSVDataProcessor();
csvProcessor.process("CSV data");
System.out.println();
DataProcessor jsonProcessor = new JSONDataProcessor();
jsonProcessor.process("XML data");
}
}
Template Method 패턴 요약
Template Method 패턴은 알고리즘의 골격(공통된 흐름)을 상위 클래스에서 정의하고, 세부적인 구현은 하위 클래스에서 담당하게 하는 행동(Behavioral) 디자인 패턴이다.
-
[baekjoon] 붙임성 좋은 총총이 (백준 26069 java 풀이)
intro : Set은 참 훌륭한 자료구조이다. (빠르고 중복제거도 해줌)
백준 문제링크
문제
총총이는 친구 곰곰이의 소개로 제2회 곰곰컵에 출연할 기회를 얻었다! 총총이는 자신의 묘기인 무지개 댄스를 선보여, 여러분의 환심을 사려 한다. 이 댄스는 중독성이 강하기 때문에, 한번 보게 된 사람은 모두 따라 하게 돼버린다. 무지개 댄스를 추는 총총 2마리 사람들이 만난 기록이 시간 순서대로 N개 주어진다. (총총이는 토끼이지만 이 문제에서는 편의상 사람이라고 가정한다.) 무지개 댄스를 추지 않고 있던 사람이 무지개 댄스를 추고 있던 사람을 만나게 된다면, 만난 시점 이후로 무지개 댄스를 추게 된다. 기록이 시작되기 이전 무지개 댄스를 추고 있는 사람은 총총이 뿐이라고 할 때, 마지막 기록 이후 무지개 댄스를 추는 사람이 몇 명인지 구해보자!
입력
첫번째 줄에는 사람들이 만난 기록의 수 N (1 <= N <= 1000)이 주어진다. 두번째 줄부터 N개의 줄에 걸쳐 사람들이 만난 기록이 주어진다. i + 1번째 줄에는 i번째로 만난 사람들의 이름 A와 B가 공백을 사이에 두고 주어진다. A와 B는 숫자와 영문 대소문자로 이루어진 최대 길이 20의 문자열이며, 서로 같지 않다. 총총이의 이름은 ChongChong으로 주어지며, 기록에서 1회 이상 주어진다. 동명이인은 없으며, 사람의 이름은 대소문자를 구분한다.(ChongChong과 chongchong은 다른 이름이다.)
출력
마지막 기록 이후 무지개 댄스를 추는 사람의 수를 출력하라.
문제 풀이 (124ms)
import java.io.*;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
String stand = "ChongChong";
// 중복제거를 위한 set 변수 선언
// 무지개 댄스를 춘 사람들 및 전염시킬 수 있는 사람들의 모임
Set<String> set = new HashSet<>();
// ChongChong 미리 add
set.add(stand);
// 입력받을 값 개수
int count = Integer.parseInt(br.readLine());
StringTokenizer st;
for (int i = 0; i < count; i++) {
st = new StringTokenizer(br.readLine());
String first = st.nextToken();
String second = st.nextToken();
// 만약 무지개 댄스를 추고 있는사람들 이거나 전염된 사람들이라면
if (set.contains(first) || set.contains(second)) {
// 만남 사람들 또한 전염
set.add(first);
set.add(second);
}
}
// bw로 출력하기 위해 문자열로 변환 후 write
bw.write(String.valueOf(set.size()));
bw.flush();
// 자원 반납
bw.close();
br.close();
}
}
문제 해석
처음에 문제를 너무 쉽게 생각해서 빠르게 코드 제출하고 틀렸었는데 입력예제 1번에 속았던거 같다. 총총이가 나온다음 부터 모든 사람들은 전염되어 그냥 카운팅만 했었는데, 생각해보니까 총총이와 만난 사람들이 다음번에도 연속적으로 다른사람들과 만난다는 전제는 없으니, 전염된 사람들을 정확히 체크해서 전염된 사람들이 입력값으로 들어오는지 확인하여 카운팅을 해야한다.
위 말이 조금 이해가 안갈수도 있으니 좀더 명확하게 설명하자면, 총총이가 만난사람이 A라고 한다면 현재 무지개 댄스를 추는 사람은 총총이와 A다. 다음 입력값으로 B와 C가 들어온다면 이사람들은 총총이와 A를 만난게 아니니 무지개 댄스를 추지 않는다 하지만 다음 입력값으로 A와 D가 들어온다면 A는 총총이에게 전염당해 무지개댄스를 추구있는사람이니 D또한 무지개 댄스를 추게된다. (이후 같은 과정이 반복됨)
위 내용을 코드로 구현하게되면, 초기에 총총이를 set변수 (전염된 사람들의 모임)에 넣어두고 만나는 사람들이 set에 있는 사람들인지 확인하며 만약 set변수에 있는사람을 만났다는건 무지개 댄스를 추고 있는 사람이라는 것이니 만난 사람들 전부 set변수에 넣는다. 여기서 중복을 걱정할 수도 있지만. set은 중복을 제거하고 데이터를 관리하는 자료구조이기에 마음편히 set에 다 넣어서 관리해도 중복을 제거한 값들만 가지고 있게되어 결과적으로 size() 메서드를 통해 무지개 댄스를 추고 있는 사람들의 개수를 구할 수 있다.
-
-
[baekjoon] 약수 (백준 1037 java 풀이)
intro : 진짜 약수의 최소값과 최대값에 집중해보자.
백준 문제링크
문제
양수 A가 N의 진짜 약수가 되려면, N이 A의 배수이고, A가 1과 N이 아니어야 한다. 어떤 수 N의 진짜 약수가 모두 주어질 때, N을 구하는 프로그램을 작성하시오.
입력
첫째 줄에 N의 진짜 약수의 개수가 주어진다. 이 개수는 50보다 작거나 같은 자연수이다. 둘째 줄에는 N의 진짜 약수가 주어진다. 1,000,000보다 작거나 같고, 2보다 크거나 같은 자연수이고, 중복되지 않는다.
출력
첫째 줄에 N을 출력한다. N은 항상 32비트 부호있는 정수로 표현할 수 있다.
문제 풀이 (104ms)
import java.io.*;
import java.util.Arrays;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
// 첫번째로 받는 입력값은 진짜약수의 개수
int count = Integer.parseInt(br.readLine());
// 진짜 약수를 보관할 배열 선언
int[] array = new int[count];
// 두번째 입력값으로
StringTokenizer st = new StringTokenizer(br.readLine());
for (int i = 0; i < array.length; i++) {
array[i] = Integer.parseInt(st.nextToken());
}
// 배열을 정렬한다 (오름차순)
Arrays.sort(array);
// 구하고자 하는 N 값 = 배열의 첫번째 값 * 마지막값
int result = array[0] * array[array.length - 1];
// bw로 출력하기 위해 문자열로 변환 후 write
bw.write(String.valueOf(result));
bw.flush();
// 자원 반납
bw.close();
br.close();
}
}
문제 해석
처음에는 진짜약수 라는 말이 무슨말인지 이해가 잘 안갔는데, 결국 요약하자면 1과 본인자신의 숫자를 제외한 나머지 약수들을 진짜약수 라고 하는것을 알게 되자마자 문제가 눈에 좀 들어왔다.
내가 구하고자 하는 값은 진짜 약수들이 주어졌을때 해당 약수들을 갖는 원래의 수 N을 찾는것인데 만약 16이라는 N이 있다면 입력값으로 진짜 약수의 개수 3과 2,4,8이 주어질것이다. 그렇다면 N의 값은 진쨔약수를 오름차순으로 정렬했을때 첫번째 값 * 마지막 값이 = N이라는 것을 알 수 있다.
이건 문제에서 주어진 입력값과 출력값인데 내가 생각한 방법을 대입해보면 다음과 같은 결과를 얻을 수 있다.
예제입력 1 : 6
예제입력 2 : 3 4 2 12 6 8
예제입력 2 오름차순 정렬 : 2 3 4 6 8 12
예제입력 2의 첫번째 값과 마지막값을 곱 연산한 결과는 : 24
예제 출력값이 24 이므로 일치한다.
위 내용을 기반으로 코드의 구현 방향을 정하였고, 가장 작은 약수와 가장 큰 약수를 곱하면 원래의 숫자 𝑁 을 정확히 계산할 수 있게 되었다.
-
[baekjoon] 창문 닫기 (백준 13909 java 풀이)
intro : 이런 문제들 너무나도 싫다.
백준 문제링크
문제
서강대학교 컴퓨터공학과 실습실 R912호에는 현재 N개의 창문이 있고 또 N명의 사람이 있다. 1번째 사람은 1의 배수 번째 창문을 열려 있으면 닫고 닫혀 있으면 연다. 2번째 사람은 2의 배수 번째 창문을 열려 있으면 닫고 닫혀 있으면 연다. 이러한 행동을 N번째 사람까지 진행한 후 열려 있는 창문의 개수를 구하라. 단, 처음에 모든 창문은 닫혀 있다. 예를 들어 현재 3개의 창문이 있고 3명의 사람이 있을 때, 1번째 사람은 1의 배수인 1,2,3번 창문을 연다. (1, 1, 1) 2번째 사람은 2의 배수인 2번 창문을 닫는다. (1, 0, 1) 3번째 사람은 3의 배수인 3번 창문을 닫는다. (1, 0, 0) 결과적으로 마지막에 열려 있는 창문의 개수는 1개 이다.
입력
첫 번째 줄에는 창문의 개수와 사람의 수 N(1 ≤ N ≤ 2,100,000,000)이 주어진다.
출력
마지막에 열려 있는 창문의 개수를 출력한다.
문제 풀이 (108ms)
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
// 주어지는 입력값
int input = Integer.parseInt(br.readLine());
// 제곱근의 값으로 결과값 계산
int result = (int) Math.sqrt(input);
// bw로 출력하기 위해 문자열로 변환 후 write
bw.write(String.valueOf(result));
bw.flush();
// 자원 반납
bw.close();
br.close();
}
}
문제 해석
아, 이문제는 정말이지 푸는데 굉장히 오래걸렸다.(이전에도 말했지만 직관적이지 않은 문제, 정수론 같은 방법을 통해서 푸는 문제들은 난이도에 상관없이 항상 어렵다) 단순히 반복문을 통해서 1부터 주어진 N까지의 값만큼 반복문을 돌면서 열고 닫고를 체크하자니 주어지는 값의 N의 범위가 21억까지여서 시간복잡도만 단순히 따져보아도 이렇게 푸는 문제가 아니라고 느껴지니 다른 방법을 통해 문제를 풀어야겠다고 생각이 들었다.
잘 생각해보면 결국 내가 계산해야하는 결과값은 마지막에 열려 있는 창문의 개수 이다. 그렇다면 열려있는 창문은 언제 열려있는가? 주어지는 값이 5라고 생각해봤다.
그렇다면 다음과같이 1(닫힘),2(닫힘),3(닫힘),4(닫힘),5(닫힘) 값이 주어지고 순차적으로 1~5의 배수값들은 열고 닫고가 반복된다. 이내용을 다음과같이 순서대로 표현해보았다.
입력값 : 5 인 경우
초기상태: 1(닫힘),2(닫힘),3(닫힘),4(닫힘),5(닫힘)
반복문 1: 1(열림),2(열림),3(열림),4(열림),5(열림)
반복문 2: 1(열림),2(닫힘),3(열림),4(닫힘),5(열림)
반복문 3: 1(열림),2(닫힘),3(닫힘),4(닫힘),5(열림)
반복문 4: 1(열림),2(닫힘),3(닫힘),4(열림),5(열림)
반복문 5: 1(열림),2(닫힘),3(닫힘),4(열림),5(닫힘)
마지막에 열려있는 창문의 개수 : 2
대충 여기서 살짝 감이 왔던게 아 이거 약수와 연관이 있구나, 약수의 개수가 홀수인경우는 열려있고 짝수인 경우는 닫혀있다고 뭔가 느낌이 왔다, 여기서 한번 더 다른 케이스에 대해서 검증해 보았다.
입력값 : 9 인 경우
초기상태: 1(닫힘),2(닫힘),3(닫힘),4(닫힘),5(닫힘),6(닫힘),7(닫힘),8(닫힘),9(닫힘)
반복문 1: 1(열림),2(열림),3(열림),4(열림),5(열림),6(열림),7(열림),8(열림),9(열림)
반복문 2: 1(열림),2(닫힘),3(열림),4(닫힘),5(열림),6(닫힘),7(열림),8(닫힘),9(열림)
반복문 3: 1(열림),2(닫힘),3(닫힘),4(닫힘),5(열림),6(열림),7(열림),8(닫힘),9(닫힘)
반복문 4: 1(열림),2(닫힘),3(닫힘),4(열림),5(열림),6(열림),7(열림),8(열림),9(닫힘)
반복문 5: 1(열림),2(닫힘),3(닫힘),4(열림),5(닫힘),6(열림),7(열림),8(열림),9(닫힘)
반복문 6: 1(열림),2(닫힘),3(닫힘),4(열림),5(닫힘),6(닫힘),7(열림),8(열림),9(닫힘)
반복문 7: 1(열림),2(닫힘),3(닫힘),4(열림),5(닫힘),6(닫힘),7(닫힘),8(열림),9(닫힘)
반복문 8: 1(열림),2(닫힘),3(닫힘),4(열림),5(닫힘),6(닫힘),7(닫힘),8(닫힘),9(닫힘)
반복문 9: 1(열림),2(닫힘),3(닫힘),4(열림),5(닫힘),6(닫힘),7(닫힘),8(닫힘),9(열림)
마지막에 열려있는 창문의 개수 : 3개
여기까지 손으로 그려보니 뭔가 순차적으로 그려보면 규칙이 나오지 않을까 싶어서 다음과 같이 순차적으로 그려보고 마지막에 열려있는 창문의 개수와 연관관계를 유추해보고자 하였다.
입력값 : 1 인 경우
초기상태: 1(닫힘)
반복문 1: 1(열림)
마지막에 열려있는 창문의 개수 : 1개
입력값 : 2 인 경우
초기상태: 1(닫힘),2(닫힘)
반복문 1: 1(열림),2(열림)
반복문 2: 1(열림),2(닫힘)
마지막에 열려있는 창문의 개수 : 1개
입력값 : 3 인 경우
초기상태: 1(닫힘),2(닫힘),3(닫힘)
반복문 1: 1(열림),2(열림),(열림)
반복문 2: 1(열림),2(닫힘),(열림)
반복문 3: 1(열림),2(닫힘),(닫힘)
마지막에 열려있는 창문의 개수 : 1개
입력값 : 4 인 경우
초기상태: 1(닫힘),2(닫힘),3(닫힘),4(닫힘)
반복문 1: 1(열림),2(열림),(열림),(열림)
반복문 2: 1(열림),2(닫힘),(열림),(닫힘)
반복문 3: 1(열림),2(닫힘),(닫힘),(닫힘)
반복문 4: 1(열림),2(닫힘),(닫힘),(열림)
마지막에 열려있는 창문의 개수 : 2개
입력값 : 5 인 경우
초기상태: 1(닫힘),2(닫힘),3(닫힘),4(닫힘),5(닫힘)
반복문 1: 1(열림),2(열림),3(열림),4(열림),5(열림)
반복문 2: 1(열림),2(닫힘),3(열림),4(닫힘),5(열림)
반복문 3: 1(열림),2(닫힘),3(닫힘),4(닫힘),5(열림)
반복문 4: 1(열림),2(닫힘),3(닫힘),4(열림),5(열림)
반복문 5: 1(열림),2(닫힘),3(닫힘),4(열림),5(닫힘)
마지막에 열려있는 창문의 개수 : 2개
각 숫자의 약수의 개수가 짝수인 경우는 결론적으로 닫힌 창문이 되는데, 그 이유는 초기에 닫혀있던게 짝수번 2,4,6,8 만큼 열렸다 다시 닫히기 떄문에 결론적으로는 닫힌 창문이된다. 그렇다면 약수의 개수가 홀수인경우는 1,3,5,7,9 만큼 열리고 닫히고 다시 열리기 때문에 결론적으로는 열린 창문이 된다. 결론적으로 약수의 개수가 홀수인 경우만 창문이 열린다는걸 알 수 있다.
이걸 코드로 구현해보자면 주어지는 값 N의 제곱근의 값은 결론적으로 위 과정을 반복하였을때 열려있는 창문의 개수를 구할수 있는 공식이 된다는 걸 알 수 있다.
-
[baekjoon] 가로수 (백준 2485 java 풀이)
intro : 가로수길 걷다가 이 문제가 생각날것만 같다.
백준 문제링크
문제
직선으로 되어있는 도로의 한 편에 가로수가 임의의 간격으로 심어져있다. KOI 시에서는 가로수들이 모두 같은 간격이 되도록 가로수를 추가로 심는 사업을 추진하고 있다. KOI 시에서는 예산문제로 가능한 한 가장 적은 수의 나무를 심고 싶다. 편의상 가로수의 위치는 기준점으로 부터 떨어져 있는 거리로 표현되며, 가로수의 위치는 모두 양의 정수이다. 예를 들어, 가로수가 (1, 3, 7, 13)의 위치에 있다면 (5, 9, 11)의 위치에 가로수를 더 심으면 모든 가로수들의 간격이 같게 된다. 또한, 가로수가 (2, 6, 12, 18)에 있다면 (4, 8, 10, 14, 16)에 가로수를 더 심어야 한다. 심어져 있는 가로수의 위치가 주어질 때, 모든 가로수가 같은 간격이 되도록 새로 심어야 하는 가로수의 최소수를 구하는 프로그램을 작성하라. 단, 추가되는 나무는 기존의 나무들 사이에만 심을 수 있다.
입력
첫째 줄에는 이미 심어져 있는 가로수의 수를 나타내는 하나의 정수 N이 주어진다(3 ≤ N ≤ 100,000). 둘째 줄부터 N개의 줄에는 각 줄마다 심어져 있는 가로수의 위치가 양의 정수로 주어지며, 가로수의 위치를 나타내는 정수는 1,000,000,000 이하이다. 가로수의 위치를 나타내는 정수는 모두 다르고, N개의 가로수는 기준점으로부터 떨어진 거리가 가까운 순서대로 주어진다.
출력
모든 가로수가 같은 간격이 되도록 새로 심어야 하는 가로수의 최소수를 첫 번째 줄에 출력한다.
문제 풀이 (220ms)
import java.io.*;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
// 반복 횟수 변수 선언
int forCount = Integer.parseInt(br.readLine());
// 입력값 배열 선언
int[] array = new int[forCount];
for (int i = 0; i < array.length; i++) {
array[i] = Integer.parseInt(br.readLine());
}
// 오름차순으로 정렬 (각 가로수의 순차적인 차이를 알기 위해서 정렬)
Arrays.sort(array);
// 가로수의 각 차이를 저장할 배열 선언
int[] gap = new int[array.length - 1];
// 반복문을 통해 i + 1 인덱스의 가로수와, i 인덱스의 가로수의 거리 차이를 계산
for (int i = 0; i < array.length - 1; i++) {
gap[i] = array[i + 1] - array[i];
}
int gapStand = gap[0];
for (int i = 1; i < gap.length; i++) {
// 반복문을 통해 최대공약수를 계산
gapStand = gcm(gap[i], gapStand);
}
int result = 0;
for (int g : gap) {
// 각 가로수간의 차이를 최대공약수로 나눈 몫에 - 1 값이 새로 심어야 하는 가로수의 개수
result += (g / gapStand) - 1;
}
// bw로 출력하기 위해 문자열로 변환 후 write
bw.write(String.valueOf(result));
bw.flush();
// 자원 반납
bw.close();
br.close();
}
// 최대공약수를 구하는 메소드
// 유클리드 호제법
// return 해야하는 값(최대 공약수)는 몫이다.
public static int gcd(int a, int b) {
while (b > 0) {
int r = a % b;
a = b;
b = r;
}
return a;
}
}
문제 해석
생각보다 까다로웠다고 느낀 문제였다. (뭔가 풀이방법이 직관적으로 보이지않는 문제들은 난이도 레벨 이런거랑 상관없이 항상 까다롭다고 느낀다.) 각 가로수 간의 차이를 최대공약수를 통해 가로수의 간격이 같을 수 있도록 할 수 있는데. 이또한 문제를 그림으로 그려보다가 알게되었다.
만약 주어진 값이 아래와 같다면 같은 간격을 유지한채로 가로수를 심으려면 결국 2만큼의 간격마다 가로수를 심어야 한다는 것을 알 수 있다. 3칸 간격으로 가로수를 심자니 1과 3 사이의 간격이 2이므로 3칸 간격으로 심을수 없고 1칸 간격으로도 심을 수 있으나, 문제의 조건이 새로 심어야 하는 최소수를 구하는 것이기에 2칸 간격으로 가로수를 심어야 한다.
입력값 : 1 3 7 13
1과 3의 차이 : 2
3과 7의 차이 : 4
7과 13의 차이 : 6
2 4 6의 최대 공약수는 ? [2]
그러면 2칸 간격마다 가로수를 새로 심게되는데 이때 다음과 같이 새로운 가로수를 심게 된다.
1 3 [5] 7 [9] [11] 13
[5],[9],[11] 3개의 가로수를 새로 심어야 한다.
근데 우리가 계산해야하는 결과는 새로심는 가로수의 개수이기에 다음과 같은 계산방법을 통해 추출 할 수 있다. (가로수 사이의 차이 값 / 최대공약수) - 1 예시를 들어보자면, 7과 3의 차이는 4이다. 4 / 최대공약수(2) - 1은 1이다. 3과 7 사이에 새로 심는 가로수의 개수는 1개이다, 13과 7의 차이는 6이다. 6 / 최대공약수(2) - 1은 2이다. 7과 13 사이에 새로 심는 가로수의 개수는 2개이다.
이렇게 최대공약수를 통해 간격을 통일하고 추가 가로수의 최소 개수를 구하는 방식으로 문제를 해결할 수 있다. 문제를 처음에는 어렵게 느꼈지만, 최대공약수 활용이라는 공식을 통해 간결하게 풀이할 수 있다는게 신기한거 같다.
-
[baekjoon] 다리 놓기 (백준 1010 java 풀이)
intro : 최적화를 해서 문제를 풀어보자.
백준 문제링크
문제
재원이는 한 도시의 시장이 되었다. 이 도시에는 도시를 동쪽과 서쪽으로 나누는 큰 일직선 모양의 강이 흐르고 있다. 하지만 재원이는 다리가 없어서 시민들이 강을 건너는데 큰 불편을 겪고 있음을 알고 다리를 짓기로 결심하였다. 강 주변에서 다리를 짓기에 적합한 곳을 사이트라고 한다. 재원이는 강 주변을 면밀히 조사해 본 결과 강의 서쪽에는 N개의 사이트가 있고 동쪽에는 M개의 사이트가 있다는 것을 알았다. (N ≤ M) 재원이는 서쪽의 사이트와 동쪽의 사이트를 다리로 연결하려고 한다. (이때 한 사이트에는 최대 한 개의 다리만 연결될 수 있다.) 재원이는 다리를 최대한 많이 지으려고 하기 때문에 서쪽의 사이트 개수만큼 (N개) 다리를 지으려고 한다. 다리끼리는 서로 겹쳐질 수 없다고 할 때 다리를 지을 수 있는 경우의 수를 구하는 프로그램을 작성하라.
입력
입력의 첫 줄에는 테스트 케이스의 개수 T가 주어진다. 그 다음 줄부터 각각의 테스트케이스에 대해 강의 서쪽과 동쪽에 있는 사이트의 개수 정수 N, M (0 < N ≤ M < 30)이 주어진다.
출력
각 테스트 케이스에 대해 주어진 조건하에 다리를 지을 수 있는 경우의 수를 출력한다.
문제 풀이1 (BigInteger를 이용한 풀이 160ms)
import java.io.*;
import java.math.BigInteger;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter, StringBuilder 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
// 반복 횟수 변수 선언
int forCount = Integer.parseInt(br.readLine());
// 반복 횟수만큼 반복문 실행
for (int i = 0; i < forCount; i++) {
// 입력값이 13 29 이런식으로 라인별로 들어오기에 StringTokenizer 객체 생성
StringTokenizer st = new StringTokenizer(br.readLine());
int first = Integer.parseInt(st.nextToken());
int second = Integer.parseInt(st.nextToken());
// 문제에서 주어진 조건은 두번쨰로 들어온 값이 첫번쨰로 들어온 값보다 크거나 같음
BigInteger value1 = factorial(second);
BigInteger value2 = factorial(second - first);
BigInteger value3 = factorial(first);
// 곱하기, 나누기 연산
BigInteger multiply = value2.multiply(value3);
BigInteger divide = value1.divide(multiply);
sb.append(divide).append("\n");
}
// bw로 출력하기 위해 sb를 문자열로 변환 후 write
bw.write(sb.toString());
bw.flush();
// 자원 반납
bw.close();
br.close();
}
public static BigInteger factorial(int n) {
// factorial 메소드를 실행하기 위한 각 변수 BigInteger 타입으로 선언
BigInteger result = BigInteger.ONE;
BigInteger inputN = BigInteger.valueOf(n);
BigInteger zero = BigInteger.ZERO;
BigInteger one = BigInteger.ONE;
// BigInteger는 compareTo 메소드로 비교
while (inputN.compareTo(zero) > 0) {
result = result.multiply(inputN);
inputN = inputN.subtract(one);
}
return result;
}
}
문제 해석1
문제의 내용의 주된 포인트는 왼쪽 사이트의 지점에 모두 다리가 건설이 되어야 한다 라는 점이 포인트다. 그말은 즉슨 오른쪽 다리에서 왼쪽다리는 무조건 선택해서 다리를 지어야 하기에, 중복되지않게 다리를 지으려면 오른쪽 다리의 사이트 포인트 중에서 왼쪽 다리의 사이트 포인트 만큼의 조합의 개수를 구하면 된다는 말로 귀결된다. 결국 이 문제 또한 조합의 갯수를 구하는 문제이며 해당 문제를 이해했다면 구현 로직으로 넘어가면 된다.
구현 로직에서 함정이 있는데, 보통 아무리 큰 값을 계산하더라도 long 타입으로 어느정도는 계산이 되는데, 30!을 계산하려면 long 을 통해 계산을 할 수 없다.(아래 그림을 참고하면 30!의 값이 얼마나 큰 값인지 알 수 있다.) 그렇다면 그보다 더 큰 데이터 범위를 담을 수 있는 객체를 사용해야 하는데 해당 문제를 풀기위해 BigInteger 타입을 사용하여 계산해 주었다.
문제 풀이2 (최적화를 통한 풀이 112ms)
import java.io.*;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter, StringBuilder 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
// 반복 횟수 변수 선언
int forCount = Integer.parseInt(br.readLine());
// 반복 횟수만큼 반복문 실행
for (int i = 0; i < forCount; i++) {
// 입력값이 13 29 이런식으로 라인별로 들어오기에 StringTokenizer 객체 생성
StringTokenizer st = new StringTokenizer(br.readLine());
int first = Integer.parseInt(st.nextToken());
int second = Integer.parseInt(st.nextToken());
// 조합을 계산하는 최적화 메소드 호출
long result = combination(second, first);
sb.append(result).append("\n");
}
bw.write(sb.toString());
bw.flush();
bw.close();
br.close();
}
public static long combination(int second, int first) {
// 결과값 변수 1 선언
long result = 1;
for (int i = 0; i < first; i++) {
// 분자 계산
result *= second - i;
// 분모 계산
result /= i + 1;
}
return result;
}
}
문제 해석2
위 코드에서 팩토리얼의 불필요한 연산과정을 줄이고 BigInteger 타입을 사용하지 않더라도 long 타입으로 연산을 할 수 있는 방법이 존재한다. 문제풀이1에서는 현재 순차적으로 계산을 진행하다보니, 큰 결과값이 도출되어 어쩔수 없이 BigInteger을 사용했는데 다음과 같은 방법을 사용하면 long 타입으로도 문제를 해결 할 수 있다.
먼저 어떻게 중복된 연산과정을 줄였는지 아래 이미지를 보면서 알아보자.
만약 10개의 카드중에 3개의 카드의 조합을 구하는 문제가 있다면 위 계산을 적용할 것이다. 근데 잘 생각해보면 분자와 분모에서 중복되어 약분이 되는 부분이 있는데, 현재 상황에서는 아래 이미지처럼 바로 7!이 약분되어 나머지 부분만 연산을 진행하면 최적화된 계산을 진행할 수 있다.
해당 내용을 정리하자면 결국 10개의 카드중에 3개의 카드를 뽑는 조합을 구하는 문제를 풀기위한 연산은 10~8 까지의 팩토리얼 연산과(분자) 1~3까지의 팩토리얼 연산(분모)만 하면 최적화된 결과를 구할 수 있음을 알 수 있다.
-
-
-
[baekjoon] 녹색거탑 (백준 24723 java 풀이)
intro : 문제가 암청 거창한거에 비해 풀이는 간단하다.
백준 문제링크
문제
Naver D2를 아시나요? D2는 For Developers, By Developers의 약자로, 개발자들을 위해 개발자들이 직접 만들어 가고 있는 네이버 개발자 지원 프로그램입니다. 네이버가 축적한 기술과 지식을 공유하고, 외부 개발자들을 지원해 대한민국 개발자 역량 강화를 이끌고, 이를 통해 업계 전체와 네이버가 함께 성장하는 선순환 구조를 만들고자 합니다. 사실 네이버의 개발자 지원은 오랜 기간 꾸준히 이어져 왔습니다. 개발자 컨퍼런스 DEVIEW를 비롯, 오픈 소스와 개발 도구 공개, 학회 및 커뮤니티 지원 등 여러 지원 프로그램이 있었습니다. 이런 다양한 프로그램을 하나로 통합한 것이 바로 Naver D2입니다. 2022년 봄 어느 날. 전 세계에 코딩괴물이 나타났다. 그리고 코딩괴물과 함께 갑작스레 등장한 ‘그것’… 바로 녹색거탑이다. 녹색거탑의 정상에서는 매년 NAVER가 개최하는 개발자 컨퍼런스 DEVIEW가 열린다. 이 DEVIEW에 참여하면, 코딩에 깊은 깨달음을 얻어 코딩괴물이 될 수 있다고 전해진다. 그리고 코딩괴물은 녹색거탑의 정상에서 내려온다. 예전부터 전해 내려오는 D2 비전서에 의하면, 코딩괴물이 녹색거탑의 정상에서 내려오는 경우의 수를 파악한 사람은, 개발자 컨퍼런스 DEVIEW에 참여할 수 있다 한다. 그리고 DEVIEW에 참여해 본인도 코딩괴물이 될 수 있다! 녹색거탑은 위 그림과 같이 규칙적으로 쌓여있다. 그림의 시야에 보이지 않는 블록은 없다. 그림의 시야에 보이는 블록의 윗면만 이용해 녹색거탑을 내려올 수 있다. 녹색거탑이 N층이면, 총 N개의 블록을 이용한 최단 경로로만 내려온다. 녹색거탑을 내려올 때는 정상에서 시작해 노란색 바닥까지, 항상 인접한 아래층의 블록으로만 내려온다. 녹색거탑을 정복하고 DEVIEW에 참여하자.
입력
녹색거탑의 높이를 나타내는 정수 N이 주어진다. (1 <= N <= 5)
출력
녹색거탑의 정상에서 바닥으로 내려오는 경우의 수를 출력한다.
문제 풀이 (100ms)
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
// 입출력을 위한 BufferedReader, BufferedWriter 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
// br을 통해 읽은 값 정수로 변환
int input = Integer.parseInt(br.readLine());
// 최단 경로로 갈수 있는 경우의 수 계산
// Math.pow는 return 값이 double 이기에 (int)로 결과값 명시적 형변환
int result = (int) Math.pow(2, input);
// bw로 출력하기 위해 문자열로 변환 후 write
bw.write(String.valueOf(result));
bw.flush();
// 자원 반납
bw.close();
br.close();
}
}
문제 해석
문제에서 주어진 그림과, 문제의 주된 내용 녹색거탑이 N층이면, 총 N개의 블록을 이용한 최단 경로로만 내려온다., 항상 인접한 아래층의 블록으로만 내려온다. 를 생각해보면 이진트리를 떠올릴 수 있다. 5층의 녹색거탑을 내려온다면 무조건 5개의 블록을 이용해 내려와야 하는데 이때 5개의 블록을 내려올 수 있는 경우의 수는 1칸단 2개의 경우의 수가 존재하기에 층마다 제곱배로 경우의 수가 증가한다. 이를 순차적으로 표현하면 아래와 같다.
1층 2개
2층 4개
3층 8개
4층 16개
5층 32개
결국에는 문제의 최단경로로 내려올 수 있는 경우의 수를 구하는 계산 공식은 2^N(2의N승) 이라고 볼 수 있다.
-
-
-
[java] 스코프, 형변환
intro : java 스코프, 형변환에 대한 개념을 알아보자.
스코프1 - 지역 변수와 스코프
변수는 선언한 위치에 따라 지역 변수, 멤버 변수(클래스 변수, 인스턴스 변수)와 같이 분류된다. 지역 변수는 이름 그대로 특정 지역에서만 사용할 수 있는 변수라는 뜻이다. 그 특정 지역을 벗어나면 사용할 수 없다. 여기서 말하는 지역이 바로 변수가 선언된 코드 블록 ({ })이다. 지역 변수는 자신이 선언된 코드 블록 안에서만 생존하고, 자신이 선언된 코드 블록을 벗어나면 제거된다. 따라서 이후에는 접근 할 수 없다.
public class Scope1 {
public static void main(String[] args) {
int m = 10; //m 생존 시작
if (true) {
int x = 20; //x 생존 시작
System.out.println("if m = " + m); //블록 내부에서 블록 외부는 접근 가능
System.out.println("if x = " + x);
} //x 생존 종료
//System.out.println("main x = " + x); //오류, 변수 x에 접근 불가
System.out.println("main m = " + m);
} //m 생존 종료
}
정리하자면 지역변수는 본인의 코드블록 안에서만 생존하고 자신의 코드블록을 벗어나면 제거되기 때문에 접근할 수 없다. 이렇게 변수의 접근 가능한 범위를 스코프(Scope) 라고 한다. 참고로 번역하면 범위라는 뜻이다. 아래 코드에서는 변수 m은 main 메소드 안에서 접근 가능한 지역변수 이지만, 변수 i는 for문 안에서만 접근 가능한 지역변수이다.
public class Scope2 {
public static void main(String[] args) {
int m = 10;
for (int i = 0; i < 2; i++) { //블록 내부, for문 내
System.out.println("for m = " + m); //블록 내부에서 외부는 접근 가능
System.out.println("for i = " + i);
} //i 생존 종료
//System.out.println("main i = " + i); //오류, i에 접근 불가
System.out.println("main m = " + m);
}
}
스코프2 - 스코프 존재 이유
변수를 선언한 시점부터 변수를 계속 사용할 수 있께 해도 되지 않을까? 왜 복잡하게 접근 범위라는 개념을 만들었을까?
public class Scope3_1 {
public static void main(String[] args) {
int m = 10;
int temp = 0;
if (m > 0) {
temp = m * 2;
System.out.println("temp = " + temp);
}
System.out.println("m = " + m);
}
}
위 코드를 보면 변수 m을 2배로 증가해서 출력하는 코드이다. 여기서 두배 증가한 값을 저장하기 위해 변수 temp를 사용하였는데, 해당 변수의 사용범위는 if 조건 안에서만 사용된다. 그런데 현재 temp 변수는 main 코드 블록안에 선언되어 있다. 이런경우 다음과 같은 문제가 발생한다.
비효율적 메모리 사용
temp는 if 코드 블록에서만 필요하지만 main 코드 블록이 종료될떄까지 메모리에 유지된다. 따라서 불필요한 메모리가 낭비된다. 만약 if 조건안에 temp를 선언했다면 더 효율적으로 메모리를 사용할 수 있다.
코드 복잡성 증가
좋은 코드는 군더더기가 없는 코드이다. temp변수는 if 조건 안에서만 필요하고 여기서만 사용하면 된다. 만약 if 조건안에서 temp 변수를 선언했다면 if 가 끝나고 temp를 전혀 생가하지 않아도 된다. 현재 위 코드는 간단한 코드이기에 복잡성이 증가한다는 말에 동의하기 어렵겠지만 실무환경에서는 하나라도 복잡성을 줄이는게 개발 생산성에 도움을 준다.
while문 vs for문 - 스코프 관점
아래 코드의 변수 스코프 관점에서 카운터 변수 i를 살펴보자. while문 예쩨에서는 i가 main 메소드 안에 선언되어 있고, for문 예제에서는 for문안에 선언되어 있다. 이런경우는 스코프의 제한이 for문이 더 제한적이기에 메모리 효올과 유지보수 관점에서 더 좋은점을 가지고 갈 수 있다.
// while문 예제
public class While2_3 {
public static void main(String[] args) {
int sum = 0;
int i = 1;
int endNum = 3;
while (i <= endNum) {
sum = sum + i;
System.out.println("i=" + i + " sum=" + sum);
i++;
}
//... 아래에 더 많은 코드들이 있다고 가정
}
}
// for문 예제
public class For2 {
public static void main(String[] args) {
int sum = 0;
int endNum = 3;
for (int i = 1; i <= endNum; i++) {
sum = sum + i;
System.out.println("i=" + i + " sum=" + sum);
}
//... 아래에 더 많은 코드들이 있다고 가정
}
}
형변환1 - 자동 형변환
작은 범위에서 큰 범위로는 당연히 값을 넣을 수 있다.(int 👉 long 👉 double) 큰 범위에서 작은 범위는 다음과 같은 문제가 발생할 수 있다. (소수점버림, 오버플로우)
작음 범위에서 큰 범위로 대입은 허용한다.
자바에서 숫자를 표현할수 있는 범위는 다음과 같다. int < long < double int 보다는 long 이, long 보다는 double 이 더 큰 범위를 표현할 수 있다. 작은 범위에서 큰 범위에 값을 대입하는 코드는 실행하면 특별한 문제없이 잘 수행된다. 자바는 기본적으로 같은 타입에 값을 대입할 수 있다. 그런데 다른 타입에 값을 대입하면 어떻게 될까? int long 을 비교해보면 long 이 int 보다 더 큰 숫자 범위를 표현한다. 작은 범위 숫자 타입에서 큰 범위 숫자 타입에 대입을 하면 문제가 되지 않는다. 만약 이런 경우까지 오류가 발생한다면 개발이 너무 불편할 것이다. long double 의 경우에도 double 은 부동 소수점을 사용하기 때문에 더 큰 숫자 범위를 표현한다. 따라서 대입할 수 있다. 정리하면 작은 범위에서 큰 범위로의 대입은 자바 언어에서 허용한다. 쉽게 이야기하면 큰 그릇은 작은 그릇에 담긴 내용물을 담을 수 있다.
public class Casting1 {
public static void main(String[] args) {
int intValue = 10;
long longValue;
double doubleValue;
longValue = intValue; // int -> long
System.out.println("longValue = " + longValue); //longValue = 10
doubleValue = intValue; // int -> double
System.out.println("doubleValue1 = " + doubleValue); //doubleValue1 = 10.0
doubleValue = 20L; // long -> double
System.out.println("doubleValue2 = " + doubleValue); //doubleValue2 = 20.0
}
}
자동형변환 (묵시적 형변환)
하지만 결국 대입하는 형(타입)을 맞추어야 하기 때문에 개념적으로는 다음과 같이 동작한다.
doubleValue = intValue
doubleValue = (double) intValue //형 맞추기
doubleValue = (double) 10 //변수 값 읽기
doubleValue = 10.0 //형변환
형변환2 - 명시적 형변환
큰 범위에서 작은 범위 대입은 명시적 형변환이 필요하다 만약 실수형 데이터를 정수형 타입에 담으면 어떻게 될까? double 타입의 데이터를 int에 담아보겠다. 명시걱으로 타입을 지정해서 형변환을 해주지 않으면 컴파일 오류가 발생한다.
public class Casting2 {
public static void main(String[] args) {
double doubleValue = 1.5;
int intValue = 0;
//intValue = doubleValue; //컴파일 오류 발생
intValue = (int) doubleValue; //형변환
System.out.println(intValue); //출력:1
}
}
형변환
하지만 만약 이런 위험을 개발자가 직접 감수하고도 값을 대입하고 싶다면 데이터 타입을 강제로 변경할 수 있다. 예를들어서 대략적인 결과를 보고싶은데 이때 소수점을 버리고 정수로만 보고 싶을 수 있다. 우리는 아래 코드와 같이 개발자가 직접 타입을 변환하는 것을 명시적 형변환 이라고 한다.
double doubleValue = 1.5;
intValue = (int) doubleValue; //형변환, intValue에 1이 할당 됨
형변환과 오버플로우
형변환을 할때 만약 작은 숫자가 표현할 수 있는 범위를 넘어서면 어떻게 될까? 아래코드에서 변수 maxIntOver 값을 intValue에 int 타입으로 변환하게되면 오버플로우가 발생한다. 정수형에서의 오버플로우는 int 타입으로 담을수 없는 큰 값이 들어오는경우 발생한다. 그렇기에 오버플로우가 발생하지 않도록 변수의 타입을 적절하게 잘 지정해 주어야 한다.
public class Casting3 {
public static void main(String[] args) {
long maxIntValue = 2147483647; //int 최고값
long maxIntOver = 2147483648L; //int 최고값 + 1(초과)
int intValue = 0;
intValue = (int) maxIntValue; //형변환
System.out.println("maxIntValue casting=" + intValue); //출력:2147483647
intValue = (int) maxIntOver; //형변환
System.out.println("maxIntOver casting=" + intValue); //출력:-2147483648
}
}
오버플로우, 언더플로우 조금만 더 알아보자!
정수 (시계열이라고 생각하자)
오버플로우 : 최댓값을 넘으면 최솟값으로 되돌아갑니다.
언더플로우 : 최솟값을 넘으면 최댓값으로 되돌아갑니다.
실수 (표현 못하는 숫자가 되니까 무한대 혹은 0으로 표현한다고 생각하자)
오버플로우 : 값이 너무 커지면 Inf(무한대)가 됩니다.
언더플로우 : 값이 너무 작아지면 서브노멀 값으로 표현되거나 0에 가까워집니다.
계산과 형변환
형변환은 대입 뿐만 아니라, 계산을 할 때도 발생한다. 알고리즘 문제 풀이시 오버플로우가 발생한다면 해당 개념이 부족해서 많이 발생하는거라고 볼 수 있다. (사실 내 얘기다…) 자바에서 계산은 다음과 같은 2가지를 기억하면 된다. 같은 타입끼리의 계산은 같은 타입의 결과를 낸다, 서로다른 타입의 계산은 큰 범위로 자동형변환이 일어난다.
public class Casting4 {
public static void main(String[] args) {
int div1 = 3 / 2;
System.out.println("div1 = " + div1); //1
double div2 = 3 / 2; // 정수끼리의 연산결과를 실수형 double에 담을 뿐이기에 결과는 1.0 이다.
System.out.println("div2 = " + div2); //1.0
double div3 = 3.0 / 2; // 실수형과, 정수형의 연산은 실수형의 결과로 반환된다.
System.out.println("div3 = " + div3); //1.5
double div4 = (double) 3 / 2;
System.out.println("div4 = " + div4); //1.5
int a = 3;
int b = 2;
double result = (double) a / b;
System.out.println("result = " + result); //1.5
}
}
-
[jpa] 연관관계 매핑 기초
intro : jpa의 연관관계 매핑에 대해 알아보자.
연관관계가 필요한 이유
다음과 같은 상황에 대해서 생각해보자. 회원과 팀이 있고 회원은 하나의 팀에만 소속될 수 있다. 회원과 팀은 다대일 관계이다. 이 내용을 토대로 객체를 테이블에 맞추어 모델링한 결과는 다음과 같다.
참조 대신에 외래키를 그대로 사용하여 Entity를 구성
외래키 식별자를 직접 다룸
식별자로 다시 조회, 객체 지향적인 방법은 아님
위 방법이 왜 객체지향적인 방법이 아니라고 말하는걸까 ?
객체는 참조를 사용해서 연관된 객체를 찾는다.
구성된 클래스에서 teamId를 통해 Member 클래스와 Team 클래스의 연관관계를 지정하고 있다. 이는 사실상 데이터베이스의 연관관계 방식을 객체에 그대로 적용한 것으로, 객체지향적 관계와는 거리가 멀다고 할 수 있다. 데이터베이스 중심의 매핑은 객체의 본질적인 특징인 캡슐화와 추상화를 무시하고, 단순히 데이터 저장소의 구조를 따라가는 방식이다. JPA를 활용할 때는 객체지향적 연관관계 매핑을 통해 객체의 본질을 유지하고 데이터 처리의 복잡성을 줄이는 것이 중요하다. 객체의 연관성을 직접 매핑하고, 이를 통해 자연스러운 탐색과 유지보수 가능한 설계를 구현해야 한다. 이는 객체지향적인 코드와 데이터베이스 간의 간극을 효과적으로 줄여주며, 장기적으로 더 나은 성능과 가독성을 제공한다.
단방향 연관관계
위 상황과 다르게 이번엔 객체 중심으로 모델링한 결과는 다음과 같다.
객체의 참조와 테이블의 외래 키를 매핑 (위 상황과 다르게 객체 자체로 키 매핑)
연관관계 저장
참조로 연관관계 조회 - 객체 그래프 탐색
연관관계 수정
양방향 연관관계와 연관관계 주인
이번에는 객체가 단방향 연관관계가 아니라, 양방향 연관관계인 경우이다. 테이블은 외래키 하나로 양쪽 테이블의 데이터를 조회할 수 있지만, 단방향으로 설정된 객체에서는 Member는 Team을 알 수 있어도 Team에서는 Member를 알 수 없다.
Member 엔티티는 단방향 동일
Team 엔티티는 컬렉션 추가 (members), mappedBy 사용
반대 방향으로 객체 그래프 탐색
연관관계 주인과 mappedBy
객체와 테이블간에 연관관계를 맺는 차이를 이해해야 한다.
객체의 연관관계는 2개이다, 회원에서 팀으로의 연관관계 + 팀에서 회원으로의 연관관계 2개. 테이블 관점에서는 회원에서 팀의 pk값을 가지고 있기에 연관관계는 하나지만 양방향으로 조회할 수 있다. 정리하자면 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다 그렇다면 객체에서 또한 어느 곳에서 하나로 외래키를 관리해야 한다. 이때 사용하는것이 mappedBy인데, 주인이 아닌곳에 속성을 사용하게 된다. 보통 주인을 결정할 때는 외래키가 있는 곳을 주인으로 정한다. 위 상황 에서는 Member.team이 연관관계의 주인이 된다.
양방향 매핑시 가장 많이 하는 실수
양방향 매핑시 연관관계의 주인에 값을 입력해야 한다.
순수한 객체 관계를 고려한다면 항상 양쪽 다 값을 입력해야 한다. (Member에 Team을 설정해 줘야 한다.)
양방향 연관관계 주의!
순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하고, 실수할 수도 있으니 연관관계 편의 메소드를 생성해서 사용하자 (권장) set 보다 change 라는 단어가 포함된 이름의 메소드를 구성하는게 관례이며 보통 연관관계 메소드는 한쪽에만 작성한다. 추가적으로 양방향 매핑시에는 무한루프를 조심해야 하는데 toString(), lombok, JSON 생성 라이브러리 등 무한 루프에 빠질 수 있다.
양방향 매핑 정리
단방향 매핑 만으로도 이미 연관관계 매핑은 가능하다. 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다. JPQL 에서 역방향으로 탐색할 일이 많다. 단방향 매핑을 잘 하고 양방향은 필요할때 추가하는게 더 좋다. (어차피 테이블 구성에 영향을 주지 않는다.)
연관관계의 주인을 정하는 기준
비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안되고, 외래키의 위치를 기준으로 정해야한다. 보통 외래키가 있는곳이 연관관계의 주인이다. 객체와 테이블의 모델링을 그림으로 한번 그려보면 외래키가 어디에 있어야 하는지 눈에 쉽게 들어오니 그려보고 판단하는게 좋아 보인다.
-
[baekjoon] 골드바흐 파티션 (백준 17103 java 풀이)
intro : 에라토스테네스의 체 방식으로 문제풀이를 성공했다.
백준 문제링크
문제
골드바흐의 추측: 2보다 큰 짝수는 두 소수의 합으로 나타낼 수 있다. 짝수 N을 두 소수의 합으로 나타내는 표현을 골드바흐 파티션이라고 한다. 짝수 N이 주어졌을 때, 골드바흐 파티션의 개수를 구해보자. 두 소수의 순서만 다른 것은 같은 파티션이다.
입력
첫째 줄에 테스트 케이스의 개수 T (1 ≤ T ≤ 100)가 주어진다. 각 테스트 케이스는 한 줄로 이루어져 있고, 정수 N은 짝수이고, 2 < N ≤ 1,000,000을 만족한다.
출력
각각의 테스트 케이스마다 골드바흐 파티션의 수를 출력한다.
문제 풀이 (252ms)
import java.io.*;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
int tcValMax = Integer.MIN_VALUE;
int tcCount = Integer.parseInt(br.readLine());
int[] tcArray = new int[tcCount];
for (int i = 0; i < tcArray.length; i++) {
tcArray[i] = Integer.parseInt(br.readLine());
if (tcArray[i] > tcValMax) {
tcValMax = tcArray[i];
}
}
boolean[] isPrime = new boolean[tcValMax + 1];
Arrays.fill(isPrime, true);
isPrime[0] = false;
isPrime[1] = false;
for (int i = 2; i * i <= isPrime.length; i++) {
if (isPrime[i]) {
for (int j = i * i; j < isPrime.length; j += i) {
isPrime[j] = false;
}
}
}
for (int tc : tcArray) {
int count = 0;
for (int i = 2; i <= tc / 2; i++) {
if (isPrime[i] && isPrime[tc - i]) {
count++;
}
}
sb.append(count).append("\n");
}
bw.write(sb.toString());
bw.flush();
bw.close();
br.close();
}
}
문제 해석
이 문제는 큰 틀의 관점에서 보았을때 2가지의 단계로 나누어 문제를 접근 할 수 있다.
첫번째 단계
조건에서 주어진 2 < N ≤ 1,000,000 에 해당하는 범위의 값들 중 소수를 판별한다.
두번째 단계
위 첫번째 문제에서 판별한 소수들 중에서 테스트 케이스로 주어진 값의 조합으로 구할 수 있는 경우의 수를 구한다. 예를들어 테스트케이스로 5가 주어졌다면, 소수인 2 + 3 의 조합으로 골드바흐 파티션을 만족하므로 1을 출력한다.
첫번째 단계를 접근하기 위해 에라토스테네스의 체에 대해서 알아야 한다.
에라토스테네스의 체 란 ?
2부터 시작해서 소수를 찾아내고 해당 소수의 배수들을 모두 제외시키는 방식으로 소수를 구한다. 이때, 해당 소수는 제외하지않는다. 이렇게 제외를 시키다보면 남은 숫자들은 전부 소수이다.
위 내용을 다음과 같이 세부 단계를 나누어 설명할 수 있다.
먼저, 2부터 n까지의 모든 수를 소수로 가정하고 배열에 표시한다. 2는 소수이므로, 2의 배수(즉, 4, 6, 8, 10 등)는 소수가 아니므로 false로 표시한다. 그다음, 3은 소수이므로, 3의 배수(즉, 6, 9, 12, 15 등)는 소수가 아니므로 false로 표시한다. 4는 이미 2의 배수로 지워졌기 때문에 건너뛰고, 그다음 5는 소수이므로, 5의 배수(즉, 10, 15, 20, 25 등)는 소수가 아니므로 false로 표시한다. 이 과정이 계속해서 반복된다.
결국 소수가 아닌것들을 제외하는 과정을 로직상으로 구현하는것이 첫번째 문제를 푸는 것의 열쇠가 된다. 위 내용을 담은 코드는 다음과 같다. (주석을 참고하자)
첫번째 단계 코드 주석
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
// 입력값을 받기위한 BufferedReader 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
// 테스트 케이스로 입력받는 값들중 가장 큰값을 구하기 위한 기준 값
int tcValMax = Integer.MIN_VALUE;
// 테스트 케이스 갯수
int tcCount = Integer.parseInt(br.readLine());
// 테스트 케이스의 값을 담을 배열
int[] tcArray = new int[tcCount];
// 테스트 케이스 갯수만큼의 반복문을 실행
for (int i = 0; i < tcArray.length; i++) {
tcArray[i] = Integer.parseInt(br.readLine());
// 테스트 케이스 값들중 가장 큰값을 구하는 로직
if (tcArray[i] > tcValMax) {
tcValMax = tcArray[i];
}
}
// 테스트 케이스 중에서 가장 큰값에 대한 배열
// +1 을 하는이유는 인덱스는 0부터 시작이기에
boolean[] isPrime = new boolean[tcValMax + 1];
// 초기는 전부 소수라고 가정 하고 시작
Arrays.fill(isPrime, true);
// 0과 1은 소수가 아님
isPrime[0] = false;
isPrime[1] = false;
// 2부터 시작해서 i * i 가 배열의 길이보다 작은지 검증
// i * i 를 하는 이유는 내부 반복문에서 이미 소수인지 검증한 값을 생략하기 위함
for (int i = 2; i * i <= isPrime.length; i++) {
// 만약 소수라면
if (isPrime[i]) {
// 해당 소수값은 제외하고 소수값의 배수들을 소수에서 제외함
for (int j = i * i; j < isPrime.length; j += i) {
isPrime[j] = false;
}
}
}
// ---------------------------- 이 아래는 차후 설명 ----------------------------
for (int tc : tcArray) {
int count = 0;
for (int i = 2; i <= tc / 2; i++) {
if (isPrime[i] && isPrime[tc - i]) {
count++;
}
}
sb.append(count).append("\n");
}
bw.write(sb.toString());
bw.flush();
bw.close();
br.close();
}
}
여기까지 이해했다면 isPrime 배열안에 소수들만 true 값을 갖게된다.
이제 두번째 단계인 골드바흐 파티션을 구하기 위해서 다음과 같은 방법을 사용하고자한다. 예제를 통해 이해해보자면, 10을 이루는 소수의 합이 있는가를 검증하고 싶다면 아래와 같이 검증한다.
소수 2 + ? = 10 여기서 ? 값은 8 이다 8은 소수가 아니기에 2와 어떤 소수와 합을 이루어 10을 이루는 골드바흐 파티션은 존재하지 않는다. 3 + ? = 10 ? 값은 7 3과 7은 둘다 소수이기에 골드바흐 파티션을 만족한다. 5 + ? = 10 ? 은 5 이기에 이 또한 골드바흐 파티션을 만족한다. 6 + ? =10 ?는 4 이기에 만족하지 못한다. 7 + ? = 10 ?는 3이기에 소수여서 골드바흐 파티션을 만족하지만 이미 3과7 조합이 나왔기에 중복된걸로 간주된다. 이 과정을 코드로 표현 하자면 다음과 같다. (위 코드와 이어진다.)
두번째 단계 코드 주석
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
// 입력값을 받기위한 BufferedReader 객체 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// 출력하기 위한 BufferedWriter 객체 생성
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
// 값을 한번에 문자열로 다루기 위한 StringBuilder 객체 생성
StringBuilder sb = new StringBuilder();
// 테스트 케이스로 입력받는 값들중 가장 큰값을 구하기 위한 기준 값
int tcValMax = Integer.MIN_VALUE;
// 테스트 케이스 갯수
int tcCount = Integer.parseInt(br.readLine());
// 테스트 케이스의 값을 담을 배열
int[] tcArray = new int[tcCount];
// 테스트 케이스 갯수만큼의 반복문을 실행
for (int i = 0; i < tcArray.length; i++) {
tcArray[i] = Integer.parseInt(br.readLine());
// 테스트 케이스 값들중 가장 큰값을 구하는 로직
if (tcArray[i] > tcValMax) {
tcValMax = tcArray[i];
}
}
// 테스트 케이스 중에서 가장 큰값에 대한 배열
// +1 을 하는이유는 인덱스는 0부터 시작이기에
boolean[] isPrime = new boolean[tcValMax + 1];
// 초기는 전부 소수라고 가정 하고 시작
Arrays.fill(isPrime, true);
// 0과 1은 소수가 아님
isPrime[0] = false;
isPrime[1] = false;
// 2부터 시작해서 i * i 가 배열의 길이보다 작은지 검증
// i * i 를 하는 이유는 내부 반복문에서 이미 소수인지 검증한 값을 생략하기 위함
for (int i = 2; i * i <= isPrime.length; i++) {
// 만약 소수라면
if (isPrime[i]) {
// 해당 소수값은 제외하고 소수값의 배수들을 소수에서 제외함
for (int j = i * i; j < isPrime.length; j += i) {
isPrime[j] = false;
}
}
}
// ------------ 여기까지가 위 코드내용과 같고 아래가 골드바흐파티션을 검증하는 로직이다 -------
// 테스트 케이스의 값을 순차적으로 꺼낸다
for (int tc : tcArray) {
// 초기 카운트 값 0으로 초기화
int count = 0;
// 소수의 합으로 tc를 이룰수 있는지 검증
// 소수의 합을 구해야 하기 떄문에 2부터 시작
// tc / 2를 하는 이유는 중복된 조합을 생략하기 위함에 있다.
for (int i = 2; i <= tc / 2; i++) {
// isPrime의 index는 각 숫자를 뜻하며 해당 index의 value 값은 소수여부 판별값이다.
// 결국 tc가 10이이고 i가 3이라면
// isPrime[3] = true, isPrime[10 - 3] = true (7은 소수이다.)
if (isPrime[i] && isPrime[tc - i]) {
// 값 증가
count++;
}
}
sb.append(count).append("\n");
}
// 출력
bw.write(sb.toString());
bw.flush();
// 객체 자원 반환
bw.close();
br.close();
}
}
-
[jpa] 엔티티 매핑
intro : jpa의 엔티티 매핑에 대해 알아보자.
엔티티 매핑 소개
객체와 테이블 매핑 : @Entity, @Table
필드와 컬럼 매핑 : @Column
기본 키 매핑 : @Id
연관관계 매핑 : @ManyToOne, @JoinColumn
객체와 테이블 매핑
@Entity
@Entity가 붙은 클래스는 JPA가 관리 엔티티라 한다.
JPA를 사용해서 테이블과 매핑할 클래스는 @Entity필수.
주의사항
기본 생성자는 반드시 존재해야한다(public, protect 생성자)
final 클래스, enum, interface, inner 클래스 사용X
저장할 필드에 final 사용
@Entity 속성 정리
속성 : name
JPA에서 사용할 엔티티 이름을 지정한다. 클래스 이름을 그대로 사용.(클래스 이름을 테이블 이름으로 사용함)
@Table
@Table은 엔티티와 매핑할 테이블 지정
속성 : name ➢ 매핑할 테이블 이름
속성 : catalog ➢ 데이터베이스 catalog 매핑
속성 : schema ➢ 데이터베이스 schema 매핑
속성 : uniqueConstraints ➢ DDL 생성시에 유니크 제약 조건 생성
데이터베이스 스키마 자동 생성
DDL을 애플리케이션 실행 시점에 자동 생성. 테이블중심에서 객체중심으로 변환. 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적합한 DDL 생성. 이렇게 생성된 DDL은 개발 장비에서만 사용. 생성된 DDL은 운영서버에서는 사용하지 않거나. 적절히 다듬은 후 사용
hibernate.hbm2ddl.auto의 옵션값으로 하기와 같은 다양한 값을 설정할 수 있다.
옵션 : create ➢ 기존테이블 삭제 후 다시 생성 (DROP + CREATE)
옵션 : create-drop ➢ create와 같으나 종료시점에 테이블 DROP
옵션 : update ➢ 변경분만 반영 (운영DB에는 사용하면 안됨)
옵션 : validate ➢ 엔티티와 테이블이 정상 매핑되었는지만 확인
옵션 : none ➢ 사용하지 않음
데이터베이스 스키마 자동 생성 - 주의
운영 장비에는 절대 create, create-auto, update 사용하면 안된다.
개발 초기 단계는 create 또는 update
테스트 서버는 update 또는 validate
스테이징과 운영 서버는 validate 또는 none
DDL 생성기능
제약 조건 추가 : 회원 이름은 필수 (10자 초과 X)
@Column(nullable = false, length = 10)
유니크 제약조건 추가
@Column(unique = true)
필드와 컬럼 매핑
매핑 어노테이션 정리
@Column : 컬럼 매핑
속성 : name ➢ 필드와 매핑할 테이블의 컬럼 이름
속성 : insertable, updateable ➢ 등록 변경 가능 여부
속성 : nullable ➢ null 값의 허용 여부 설정 false 설정시 not null 제약 조건이 붙음
속성 : unique ➢ 간단히 유니크 제약 조건 걸때 사용
속성 : columnDefinition ➢ 데이터베이스 컬럼 정보를 직접 줄 수 있음 (varchar(100))
속성 : length ➢ 문자 길이 제약조건, String 타입에만 사용
속성 : precision,scale ➢ BigDecimal 같은 타입에서만 사용
@Temporal : 날짜 타입 매핑
날짜 타입 (Date, Calendar)를 매핑할때 사용. LocalDate, LocalDateTIme을 사용할때는 생략 가능.
속성 : value ➢ TemporalType.DATE (날짜 데이터베이스 date 타입과 매핑)
속성 : value ➢ TemporalType.TIME (시간 데이터베이스 time 타입과 매핑)
속성 : value ➢ TemporalType.TIMESTAMP (날짜 데이터베이스 timestamp 타입과 매핑)
@Enumerated : enum 타입 매핑 (ORDINAL 사용 X)
속성 : value ➢ EnumType.STRING (enum 이름을 데이터 베이스에 저장)
속성 : value ➢ EnumType.ORDINNAL (enum 순서를 데이터 베이스에 저장)
@Lob : BLOB(바이너리 데이터), CLOB(문자 데이터) 매핑 - 큰 데이터 타입 지정할때 사용
매핑하는 필드 타입이 문자면 CLOB 매핑, 나머지는 BLOB 매핑
@Transient : 특정 필드를 컬럼에 매핑하지 않음 (매핑 무시)
필드 매핑 X
데이터베이스에 저장 X 조회 X
주로 메모리상에 임시로 어떤 값을 보관하고 싶을 때 사용
기본 키 매핑
기본 키 매핑 어노테이션
@Id, @GeneratedValue
기본 키 매핑 방법
직접 할당: @Id만 사용
자동 생성: @GeneratedValue
IDENTITY 전략 - 특징
IDENTITY : 데이터베이스에 위임 MYSQL
기본 키 생성을 데이터베이스에 위임. 주로 MYSQL, PostgreSQL, SQL Server, DB2에서 사용 AUTO_INCREMENT는 데이터베이스에 INSERT SQL을 실행한 이후에 ID 값을 알 수 있음.
예외적으로 JPA는 IDENTITY 전략을 사용하는 경우 em.persite() 하는 시점에서 미리 SQL 실행 후, 식별자 조회를 한다 그 이유는 SQL 을 실행하고나서 PK 값을 알 수 있기 떄문에 JPA 내부적으로 SQL을 선 실행하고, PK 값을 영속성 컨텍스트에 담는다.
SEQUENCE 전략 - 특징
SEQUENCE : 데이터베이스 시퀀스 오브젝트 사용, ORACLE
데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트
SEQUENCE 전략 - 매핑
@SequenceGenerator
속성 : name ➢ 식별자 생성기 이름
속성 : sequenceName ➢ 데이터베이스에 등록되어 있는 시퀀스 이름
속성 : initialValue ➢ 처음 시작 값 보통 1로 지정
속성 : allocationSize ➢ 시퀀스값이 1씩 증가로 설정되어 있다면 1로 설정해야함 (성능 최적화에 사용)
속성 : catalog, schema ➢ 데이터 베이스 catalog, schema 이름
TABLE 전략 - 매핑
TABLE : 키 생성용 테이블 사용, 모든 DB에서 사용
키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략
장점 : 모든 데이터베이스에 적용 가능
단점 : 성능이 안좋음
AUTO 전략
AUTO : 방언에 따라 자동 지정, 기본값 (따로 설정 안해도 됨)
권장하는 식별자 전략
기본 키 제약 조건 : null 아님, 유일, 변하면 안된다.
미래까지 이 조건을 만족하는 자연키는 찾기 어렵다. 대리키를 사용하자.
예를들어 주민등록번호도 기본키로 적절하지 않다.
권장: Long형 + 대체키 + 키 생성전략 사용
-
[java] 반복문
intro : java 반복문에 대한 개념을 알아보자.
반복문 시작
반복문은 이름 그대로 특정 코드를 반복해서 실행할 떄 사용한다. 자바는 다음 3가지 종류의 반복문을 제공한다.
while , do-while , for
위와 같은 반복문들을 어떤 상황에서 어떻게 사용 할 수 있는지 차근차근 알아보자.
while문 1
while 문은 조건에 따라 코드를 반복해서 실행할 때 사용한다. 조건식을 확인 하였을 때 참이면 코드 블럭을 실행하고 거짓이면 while문을 벗어난다. 만약 조건식이 참 인경우 코드 블록을 실행한 이후에 다시 조건식 검사로 들어가서 조건식을 검사한다.무한반복
while (조건식) {
// 코드
}
좀 더 실제 개발 환경에서 만나볼 수 있을 것 같은 예제를 보면 다음과 같은 코드를 참고 할 수 있다.
public class While1_2 {
public static void main(String[] args) {
int count = 0;
while (count < 3) {
count++;
System.out.println("현재 숫자는:" + count);
}
}
}
while문 2
while 문을 좀더 다양한 상황에 적용해보자, 만약 1부터 3까지 값을 더해야 하는 상황이 있다고 가정해보겠다 while문으로 어떻게 코드를 작성하면 좋을까 ?
public class While2_3 {
public static void main(String[] args) {
int sum = 0;
int startNum = 1;
int endNum = 3;
while (startNum <= endNum) { // startNum이 endNum(3) 이 될때까지 while문 반복
sum = sum + startNum;
System.out.println("startNum=" + startNum + " sum=" + sum);
startNum++;
}
}
}
do-while문
do-while문은 while문과 비슷하지만, 조건에 상관없이 무조건 한 번은 코드를 실행한다.
do {
// 코드 (무조건 한번은 실행된다, 이후에 while문의 조건식이 참인 경우 do 문장을 다시 실행하게 된다.)
} while (조건식);
예를 들어서 조건에 만족하지 않아도 한번은 현재 값을 출력하고 싶다고 하자. 먼저 while 문을 사용한 예제를 보겠다.
public class DoWhile1 {
public static void main(String[] args) {
int i = 10;
while (i < 3) {
System.out.println("현재 숫자는:" + i);
i++;
}
}
}
while 문의 조건에 부합하지 않기때문에 false 가 나오게 되고, 아무것도 출력되지 않는 실행결과를 볼 수 있다. 이번에는 do-while문을 사용해 보겠다. 아래와 같이 코드를 작성한다면, do-while은 do 블록의 코드를 먼저 실행하고 while문의 조건을 검사 후, 참 이면 do 블록의 코드를 실행하고 거짓 이면 do 블록의 코드를 실행하지 않는다. 그렇기에 아래 코드는 현재 숫자는: 10 이라는 결과를 출력하게 된다.
public class DoWhile2 {
public static void main(String[] args) {
int i = 10;
do {
System.out.println("현재 숫자는:" + i);
i++;
} while (i < 3);
}
}
결론적으로 do-while문은 최초 한번은 코드 블럭을 꼭 실행해야 하는 경우에 사용하면 된다.
break, continue
break와 continue는 반복문에서 사용할 수 있는 키워드다. break문은 반복문을 즉시 종료하고 나간다 (가장 가까운 반복문을 탈출한다) continue는 반복문의 나머지 부분을 건너뛰고 다음 반복문으로 진행하는데 사용한다. 해당 키워드 들은 while , do-while , for 와 같은 모든 반복문에서 사용할 수 있다.
break
아래 코드에서 코드1 실행후 break 를 만나면 코드2는 실행하지 않고 while문이 종료된다.
while (조건식) {
코드1;
break; //즉시 while문 종료로 이동한다.
코드2;
} //while문 종료
아래 코드는 sum이 10보다 크면 break문을 통해 반복문을 탈출하는 코드이다.
public class Break1 {
public static void main(String[] args) {
int sum = 0;
int i = 1;
while (true) {
sum += i;
if (sum > 10) {
break;
System.out.println("합이 10보다 크면 종료: i=" + i + " sum=" + sum);
}
i++;
}
}
}
continue
continue를 만나면 코드2 가 실행되지 않고 다시 조건식으로 이동한다. 조건식이 참이면 while 문을 실행한다.
while (조건식) {
코드1;
continue; //즉시 조건식으로 이동한다.
코드2;
}
아래 코드는 i가 3일 때 continue을 통해 반복문의 조건식으로 이동하는 코드이다.
public class Continue1 {
public static void main(String[] args) {
int i = 1;
while (i <= 5) {
if (i == 3) {
i++;
continue;
}
System.out.println(i);
i++;
}
}
}
for문1
for문도 while문과 같은 반복문이고, 코드를 반복 실행하는 역할을 한다. for문은 주로 반복 횟수가 정해져 있을 때 사용한다. for문은 다음과 같은 순서대로 실행된다.
step 1
초기식이 실행된다. 주로 반복 횟수와 관련된 변수를 선언하고 초기화 할 때 사용한다.
초기식은 딱 1번 사용된다.
step 2
조건식을 검증한다. 참이면 코드를 실행하고, 거짓이면 for문을 빠져나간다.
step 3
코드를 실행한다.
step 4
코드가 종료되면 증감식을 실행한다. 주로 초기식에 넣은 반복 횟수와 관련된 변수의 값을 증가할 때 사용한다.
step 5
다시 2. 조건식 부터 시작한다. 무한 반복
for (1.초기식; 2.조건식; 4.증감식) {
// 3.코드
}
for문은 while문을 조금 더 편하게 다룰 수 있도록 구조화 한거라고 볼 수 있는데 다음과 같은 예제를 보면 조금 더 for문 사용법에 대해서 알 수 있다. 1부터 10까지의 값을 출력하는 반복문인데 for문의 구조 순서대로 따라가면 순차적으로 1부터 10까지의 값을 출력하는 것을 알 수 있다.
public class For1 {
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
System.out.println(i);
}
}
}
for문2
for문에서 초기식, 조건식, 증감식은 선택이다.
for (초기식; 조건식; 증감식) {
// 코드
}
다음과 같이 모두 생략해도 된다. 단 생략해도 각 영역을 구분하는 세미콜론(;)은 유지해야한다
for (;;) {
// 코드
}
결국 위 반복문은 다음과 같은 코드가 된다.
while (true) {
// 코드
}
위와같은 초기식 조건식 증감식을 전부 생략하고 사용하는 코드는 어떤 상황에서 사용할까 ? 솔직히 좀 억지스럽지만 이렇게도 사용할 수 있다는 문법적인 내용으로만 이해하는게 좋은거 같다. 실제로는 거의 사용을 안한다.
아래 코드는 1부터 시작해서 숫자를 계속 누적해서 더하다가 10보다 큰 시점이 되었을떄의 i 값을 구하는 문제이다. 쉽게 얘기해서 i = 1일때 sum = 1, i = 2일때 sum = 3, i = 3일때 sum = 6, i = 4일때 sum = 10, i = 5일때는 sum = 15 가 되기에 10보다 큰 값이 처음으로 나오는 i 값인 5를 출력하면 되는 것이다. 다만 이것을 초기식 조건식 증감식을 생략한 for문으로 코드를 구현 해 보는 것이다.
public class Break2 {
public static void main(String[] args) {
int sum = 0;
int i = 1;
for (; ; ) { // 초기식 조건식 증감식 생략
sum += i;
if (sum > 10) {
break; // if 조건이 만족하였을때 반복문 탈출
System.out.println("합이 10보다 크면 종료: i=" + i + " sum=" + sum);
}
i++;
}
}
}
중첩 반복문
반복문은 내부에 또 반복문을 만들 수 있다. for, while 모두 가능하다.
public class Nested1 {
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
System.out.println("외부 for 시작 i:" + i);
for (int j = 0; j < 3; j++) {
System.out.println("-> 내부 for " + i + "-" + j);
}
System.out.println("외부 for 종료 i:" + i);
System.out.println(); //라인 구분을 위해 실행
}
}
}
다음은 위 코드의 실행 결과이며, 외부 for는 2번, 내부 for는 3번 실행된다. 그런데 외부 for 1번당 내부 for가 3번 실행되기 때문에 외부(2) * 내부(3) 해서 총 6번의 내부 for 코드가 수행된다. 이러한 중첩 반복문은 평소에도 자주 사용되고 실제 개발 환경 및 알고리즘 문제에서도 굉장히 자주 사용이 된다. 정말 많이 사용한다.
외부 for 시작 i:0
-> 내부 for 0-0
-> 내부 for 0-1
-> 내부 for 0-2
외부 for 종료 i:0
외부 for 시작 i:1
-> 내부 for 1-0
-> 내부 for 1-1
-> 내부 for 1-2
외부 for 종료 i:1
-
[design-pattern] strategy 패턴
intro : strategy 패턴에 대해서 알아보자
Strategy 패턴이란 ?
Strategy 패턴은 특정 작업을 수행하는 여러 방식(즉, 전략)을 정의하고, 필요에 따라 이를 교체하며 사용할 수 있도록 설계된 행동 패턴입니다. 쉽게 말해, 어떤 작업을 처리하는 방식(모드)을 각각의 클래스로 분리하고, 실행 시점에 적절한 전략을 선택해 사용하는 것입니다. 예를 들어, 다이소에서 물건을 결제할 때 다양한 결제 방식을 선택할 수 있는 화면이 나온다고 가정해 보겠습니다. 사용할 수 있는 결제 방식으로는 카드 결제, 현금 결제, 카카오페이 결제, 애플페이 결제, 네이버페이 결제 등이 있을 수 있습니다. 만약 이러한 결제 프로그램을 우리가 작성할 때, 결제라는 행위는 동일하지만, 선택된 결제 방식에 따라 수행되어야 하는 로직이 달라지게 됩니다. 이처럼 다양한 결제 방식을 유연하게 처리하기 위해 Strategy 패턴을 적용할 수 있습니다. Strategy 패턴을 통해 각 결제 방식을 별도 클래스로 분리하고, 실행 시점에 적절한 전략을 선택하여 사용함으로써 코드의 확장성과 유연성을 높일 수 있습니다.
Strategy 패턴은 언제 사용하는가 ?
Strategy 패턴은 행동(Behavior)을 캡슐화하고, 다양한 알고리즘(전략)을 동적으로 교체할 수 있도록 설계할 때 사용한다. 이 패턴은 특정 작업을 수행하는 여러 알고리즘(전략)들이 존재하며, 이들 사이를 유연하게 전환하거나 교체해야 할 때 유용하다.
Strategy 패턴 적용 코드 (1)
결제 전략에 사용할 인터페이스
// 결제 방식에 대한 인터페이스
interface PaymentStrategy {
void pay(int amount);
}
결제 전략 인터페이스를 구현한 실제 전략 클래스 (구현체)
// PaymentStrategy 인터페이스를 구현한 신용카드 결제 방식 클래스 구성
class CreditCardPayment implements PaymentStrategy {
private String name;
private String cardNumber;
public CreditCardPayment(String name, String cardNumber) {
this.name = name;
this.cardNumber = cardNumber;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid with credit card");
}
}
// PaymentStrategy 인터페이스를 구현한 페이팔 결제 방식 클래스 구성
class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid using PayPal");
}
}
각 전략(신용카드결제, 페이팔결제) 에 맞게 결제 메소드를 호출할수 있는 클래스
// CreditCardPayment, PayPalPayment 클래스를 통해 객체를 생성할 수 있는 ShoppingCart 클래스
// setPaymentStrategy 메소드를 통해 어떤 전략이던지 동적으로 선택할 수 있다.
// 해당 클래스의 checkout 메소드를 통해 각 전략에 맞는 결제 메소드를 실행 할 수 있다.
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
클라이언트 코드 에서 ShoppingCart 클래스 setPaymentStrategy 메소드를 통해 신용카드 결제 방식으로 결제, 두번째로는 setPaymentStrategy 메소드를 통해 페이팔 결제방식 으로 전환. 다음과 같은 코드를 통해 각 상황에 맞게 전략을 동적으로 선택 할 수 있음을 알 수 있다.
// Client code
public class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(
new CreditCardPayment(
"John Doe",
"1234567890123456"
)
);
//100 paid with credit card
cart.checkout(100);
cart.setPaymentStrategy(
new PayPalPayment(
"johndoe@example.com"
)
);
// 200 paid using PayPal
cart.checkout(200);
}
}
Strategy 패턴 적용 코드 (2)
문자열을 압축하는 프로그램을 구성할건데, 압축하는 방식을 두가지 전략으로 구성하고자 한다. 1번은 각 문자가 연속되는 횟수를 기록하여 문자열을 압축하는 방식, 2번은 문자열의 각 모음을 정해진 숫자로 변환하는 방식이 있다. 해당 상황을 가정하고 아래와 같은 코드를 따라가보자.
압축 전략에 사용할 인터페이스
interface CompressionStrategy {
String compress(String data);
}
압축 전략 인터페이스를 구현한 실제 전략 클래스 (구현체)
class RunLengthEncoding implements CompressionStrategy {
@Override
public String compress(String data) {
StringBuilder compressed = new StringBuilder();
int count = 1;
for (int i = 1; i <= data.length(); i++) {
if (i < data.length() && data.charAt(i) == data.charAt(i - 1)) {
count++;
} else {
compressed.append(data.charAt(i - 1));
compressed.append(count);
count = 1;
}
}
return compressed.toString();
}
}
class SimpleReplacementCompression implements CompressionStrategy {
@Override
public String compress(String data) {
return data.replace("a", "1")
.replace("e", "2")
.replace("i", "3")
.replace("o", "4")
.replace("u", "5");
}
}
각 전략(RunLengthEncoding, SimpleReplacementCompression) 에 맞게 압축 메소드를 호출할 수 있는 클래스
// Context class
class Compressor {
private CompressionStrategy strategy;
public void setCompressionStrategy(CompressionStrategy strategy) {
this.strategy = strategy;
}
public String compress(String data) {
return strategy.compress(data);
}
}
클라이언트 코드 에서 Compressor 클래스 setCompressionStrategy 메소드를 통해 RunLengthEncoding방식으로 압축, 두번째로는 setCompressionStrategy메소드를 통해 SimpleReplacementCompression 방식으로 전환. 결국 1번에서 확인했던 전략 패턴의 큰 틀의 구조와 같다고 볼 수 있다. 전략패턴을 적용할 상황만 살짝 다를 뿐이었다.
// Client code
public class Main {
public static void main(String[] args) {
Compressor compressor = new Compressor();
String data = "aabcccccaaa";
compressor.setCompressionStrategy(new RunLengthEncoding());
System.out.println(
"RLE Compression: " + compressor.compress(data)
);
// RLE Compression: a2b1c5a3
compressor.setCompressionStrategy(new SimpleReplacementCompression());
System.out.println(
"Simple Replacement: " + compressor.compress(data)
);
// Simple Replacement: 11bccccc111
}
}
Strategy 패턴 요약
Strategy 패턴은 동일한 작업을 수행하는 여러 방법을 정의하고, 실행 시 적합한 방법을 동적으로 선택하여 사용하는 행동 패턴이다.
-
-
-
-
-
-
[jpa] 영속성 관리(내부 동작 방식)
intro : jpa의 영속성 관리에 대해 알아보자.
JPA에서 가장 중요한 2가지
1. 객체와 관계형 데이터베이스 매핑하기
2. 영속성 컨텍스트
엔티티 매니저 팩토리와 엔티티 매니저
EntityManagerFactory
엔티티 매니저 팩토리는 JPA 애플리케이션에서 데이터베이스와의 연결을 관리하고 엔티티 매니저 객체를 생성하는 팩토리입니다. 데이터베이스와의 연결 설정, 캐싱, 그리고 여러 개의 엔티티 매니저 인스턴스를 관리하는데 사용됩니다.
EntityManager
엔티티 매니저는 JPA의 핵심 인터페이스로, 엔티티를 데이터베이스에 CRUD(Create, Read, Update, Delete) 작업을 수행하는 데 사용됩니다. 엔티티 매니저 특정 단위의 작업(예: 하나의 트랜잭션 또는 요청)을 처리할 때 사용됩니다.
영속성 컨텍스트
JPA 를 이해하는데 가장 중요한 용어, 엔티티를 영구 저장하는 환경 이라는 뜻. 영속성 컨텍스트는 논리적인 개념이며 엔티티 매니저를 통해서 영속성 컨텍스트에 접근 가능.
엔티티의 생명주기
비영속
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
Member member = new Member();
영속
영속성 컨텍스트에 관리되는 상태
em.persist(member);
준영속
영속성 컨텍스트에 저장되었다가 분리된 상태
em.detach(member);
em.clear();
em.close();
삭제
삭제된 상태
em.remove(member)
영속성 컨텍스트의 이점
1차 캐시
엔티티 매니저는 자체적으로 1차 캐시를 갖고 있습니다. 같은 엔티티 매니저 내에서 동일한 엔티티를 여러 번 조회하면, 데이터베이스에 다시 쿼리를 보내지 않고 1차 캐시에서 반환합니다.
동일성 보장
동일한 엔티티 매니저 내에서 같은 엔티티를 여러 번 조회하면 동일한 객체 인스턴스가 반환됩니다.
트랜잭션을 지원하는 쓰기 지연
엔티티 매니저는 persist()나 merge() 등으로 데이터를 수정해도 즉시 데이터베이스에 반영하지 않고, 트랜잭션이 커밋될 때 한꺼번에 SQL 쿼리를 보냅니다.
변경 감지
엔티티 매니저는 엔티티의 변경 사항을 자동으로 감지하고, 트랜잭션이 커밋될 때 변경된 내용을 데이터베이스에 반영합니다. (스냅샷과 비교하여 변경을 감지함)
지연 로딩
엔티티의 연관된 데이터가 실제로 필요할 때까지 데이터베이스 조회를 지연시킵니다. 위 특징으로 인해 불필요한 데이터 로드를 피하고 성능을 최적화할 수 있습니다.
플러시
영속성 컨텍스트의 변경 내용을 데이터 베이스에 반영. 이때 영속성 컨텍스트를 비우지않음. 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화. 트랜잭션이라는 작업단위가 중요하며, 커밋 직전에만 동기화 하면 됨.
영속성 컨텍스트를 플러시 하는 방법
em.flush() 직접 호출
트랜잭션 커밋 플러시 자동 호출
JPQL 쿼리 실행 플러시 자동 호출
JPQL 쿼리 실행시 플러시가 자동으로 호출되는 이유
JPQL 실행 시 자동 플러시는 쿼리가 실행되기 전에 EntityManager의 변경 사항이 반영되도록 하여, JPQL의 결과가 데이터의 최신 상태를 보장하도록 돕는 중요한 동작입니다.
-
[java] 조건문
intro : java 조건문에 대한 개념을 알아보자.
if문1 - if else
조건문 시작
특정 조건에 따라서 코드를 실행하려면 어떻게 해야할까? 예를들어서 만약 18살 이상이면 “성인입니다.” 를 출력하고, 만약 18살 미만이라면 “미성년자입니다.”를 출력해야 한다면? 아마도 다음과 같이 코딩을 해야 할 것이다.
// 한글
만약 (나이 >= 18)면 "성인입니다"
만약 (나이 < 18)면 "미성년자입니다"
// 영어
if (age >= 18) "성인입니다"
if (age < 18) "미성년자입니다"
이렇게 특정 조건에 따라서 코드를 실행하는 것을 조건문 이라고 한다. 조건문에는 if문 switch문이 있다. 둘다 특정 조건에 따라서 다른 코드를 실행하는 것이라 생각하면 된다.
if 문
if문은 특정 조건이 참인지 확인하고 그 조건이 참(true)일 경우 특정 코드 블록을 실행한다. 아래 코드에서는 첫번째 조건문인 if문은 참 이기에 실행이되고, 두번째 조건문에서는 거짓 이기에 실행되지 않는다.
public class If1 {
public static void main(String[] args) {
int age = 20; // 사용자 나이
if (age >= 18) { // 참
System.out.println("성인입니다.");
}
if (age < 18) { // 거짓
System.out.println("미성년자입니다.");
}
}
}
else 문
else 문은 if문에서 만족하는 조건이 없을 때 실행하는 코드를 제공한다.
if (condition) {
// 조건이 참일 때 실행되는 코드
} else {
// 만족하는 조건이 없을 때 실행되는 코드
}
아래 코드에서 사용자의 나이가 10이라면, 첫번째 if문을 만족하지 않기에, else문의 “미성년자입니다.” 문구가 출력된다.
public class If2 {
public static void main(String[] args) {
int age = 10; // 사용자의 나이
if (age >= 18) {
System.out.println("성인입니다."); //참일 때 실행
} else {
System.out.println("미성년자입니다.");//만족하는 조건이 없을 때 실행
}
}
}
if문2 - else if
else if 문은 앞선 if문의 조건이 거짓일 때 다음 조건을 검사한다. 만약 앞선 if문이 참이라면 else if 문을 실행하지 않는다. 여러개의 if문을 하나로 묶을때 이렇게 if-else문을 사용할 수 있다. 이렇게 하면 특정 조건이 만족하면 해당 코드를 실행하고 if문 전체를 빠져나온다. 특정 조건을 만족하지 않으면 다음 조건을 검사한다. 여기서 핵심은 순서대로 맞는 조건을 찾아보고 맞는 조건이 있으면 딱1개만 실행이 되는 것이다.
if (condition1) {
// 조건1이 참일 때 실행되는 코드
} else if (condition2) {
// 조건1이 거짓이고, 조건2가 참일 때 실행되는 코드
} else if (condition3) {
// 조건2이 거짓이고, 조건3이 참일 때 실행되는 코드
} else {
// 모든 조건이 거짓일 때 실행되는 코드
}
아래 코드를 보면 더 와닿게 else-if 문을 이해할 수 있다. age = 7인 경우 if(age <= 7) 의 조건이 참이다. “미취학”을 출력하고 전체 if 문 밖으로 나간다. age = 13인 경우 if(age <= 7) 의 조건이 거짓이다. 다음 조건으로 넘어간다. else if(age <= 13) 의 조건이 참이다. “초등학생”을 출력하고 전체 if 문 밖으로 나간다. **age = 50인 경우 if(age <= 7) 의 조건이 거짓이다. 다음 조건으로 넘어간다. else if(age <= 13) 의 조건이 거짓이다. 다음 조건으로 넘어간다.else if(age <= 16) 의 조건이 거짓이다. 다음 조건으로 넘어간다. else if(age <= 19) 의 조건이 거짓이다. 다음 조건으로 넘어간다. else 만족하는 조건 없이 else 까지 왔다. else 에 있는 “성인”을 출력하고 전체 if 문 밖으로 나간다.
public class If4 {
public static void main(String[] args) {
int age = 14;
if(age <= 7) { //~7: 미취학
System.out.println("미취학");
} else if(age <= 13) { //8~13: 초등학생
System.out.println("초등학생");
} else if(age <= 16) { //14~16: 중학생
System.out.println("중학생");
} else if(age <= 19) { //17~19: 고등학생
System.out.println("고등학생");
} else { //20~: 성인
System.out.println("성인");
}
}
}
if문3 - if 문과 else if문
if문에 else-if문을 함께 사용하는 것은 서로 연관된 조건일 때 사용한다. 그런데 서로 관련이 없는 독립 조건이면, else-if를 사용하지 않고 if문을 각각 따로 사용해야 한다.
아래와 같은 상황에서 if문을 독립적으로 사용한다. (else-if X)
if문을 독립적으로 적용하는 상황
온라인 쇼핑몰의 할인 시스템을 개발해야 한다. 한 사용자가 어떤 상품을 구매할 때, 다양한 할인 조건에 따라 총 할인 금액이 달라질 수 있다. 각각의 할인 조건은 다음과 같다. 아이템 가격이 10000원 이상일 때, 1000원 할인 나이가 10살 이하일 때 1000원 할인 이 할인 시스템의 핵심은 한 사용자가 동시에 여러 할인을 받을 수 있다는 점이다. 예를 들어, 10000원짜리 아이템을 구매할 때 1000원 할인을 받고, 동시에 나이가 10살 이하이면 추가로 1000원 더 할인을 받는다. 그래서 총 2000원 까지 할인을 받을 수 있다.
public class If5 {
public static void main(String[] args) {
int price = 10000;// 아이템 가격
int age = 10;//나이
int discount = 0;
if (price >= 10000) {
discount = discount + 1000;
System.out.println("10000원 이상 구매, 1000원 할인");
}
if (age <= 10) {
discount = discount + 1000;
System.out.println("어린이 1000원 할인");
}
System.out.println("총 할인 금액: " + discount + "원");
}
}
if문 - switch
switch 문은 앞서 배운 if문을 조금 더 편리하게 사용할 수 있는 기능이다. 참고로 if문은 비교연산자를 사용할 수 있지만, switch문은 단순히 값이 같은지만 비교할 수 있다. switch문은 조건식에 해딩하는 특정 값으로 실행할 코드를 선택한다. 조건식의 결과 값이 어떤 case 의 값과 일치하면 해당 case 의 코드를 실행한다. break 문은 현재 실행 중인 코드를 끝내고 switch 문을 빠져나가게 하는 역할을 한다. 만약 break 문이 없으면, 일치하는 case 이후의 모든 case 코드들이 순서대로 실행된다. default 는 조건식의 결과값이 모든 case 의 값과 일치하지 않을 때 실행된다. if 문의 else 와 같다. default 구문은 선택이다. if , else-if , else 구조와 동일하다.
public class Main {
public static void main(String[] args) {
switch (조건식) {
case value1:
// 조건식의 결과 값이 value1일 때 실행되는 코드
break;
case value2:
// 조건식의 결과 값이 value2일 때 실행되는 코드
break;
default:
// 조건식의 결과 값이 위의 어떤 값에도 해당하지 않을 때 실행되는 코드
}
}
}
실제 switch 문을 사용한 예제 코드를 살펴보자 변수 grade 값에 따라 실행되는 case문이 달라진다.
public class Switch2 {
public static void main(String[] args) {
//grade 1:1000, 2:2000, 3:3000, 나머지: 500
int grade = 2;
int coupon;
switch (grade) {
case 1:
coupon = 1000;
break;
case 2:
coupon = 2000;
break;
case 3:
coupon = 3000;
break;
default:
coupon = 500;
}
System.out.println("발급받은 쿠폰 " + coupon);
}
}
case문에 break 문이 없으면?
만약 break문이 없으면 어떻게 되는지 확인하기 위해 다음과 같이 조건을 변경해보자. 2등급과 3등급이 같이 3000원 쿠폰을 발급한다고 가정해보겠다. case2에는 break문이 없다. 그러면 중단하지 않고, 바로 다음에 있는 case3의 코드를 실행한다. 여기서 coupon = 3000을 수행하고, break문을 만나서 switch문 밖으로 빠져나간다. 발급받은 쿠폰 3000이 출력된다.
public class Switch3 {
public static void main(String[] args) {
//grade 1:1000, 2:3000(변경), 3:3000, 나머지: 500
int grade = 2;
int coupon;
switch (grade) {
case 1:
coupon = 1000;
break;
case 2:
case 3:
coupon = 3000;
break;
default:
coupon = 500;
break;
}
System.out.println("발급받은 쿠폰 " + coupon);
}
}
if문 - 삼항 연산자
if문을 사용할 때 다음과 같이 단순히 참과 거짓에 따라 특정 값을 구하는 경우가 있다. 삼항 연산자 없이 if 문만 사용해도 된다. 하지만 단순히 참과 거짓에 따라서 특정 값을 구하는 삼항 연산자를 사용하면 if 문 보다 간결한 코드를 작성할 수 있다
public class CondOp2 {
public static void main(String[] args) {
int age = 18;
String status = (age >= 18) ? "성인" : "미성년자"; // if문을 사용할 때 보다 간결한 코드 구성 가능
System.out.println("age = " + age + " status = " + status);
}
}
-
[baekjoon] 풍선 터뜨리기 (백준 2346 java 풀이)
intro : 계속 고민하게 되는 부분은 풍선의 순서 번호와, 풍선안의 이동해야하는 값을 동시에 어떻게 관리할 것인가를 생각해 보았다.
백준 문제링크
문제
1번부터 N번까지 N개의 풍선이 원형으로 놓여 있고. i번 풍선의 오른쪽에는 i+1번 풍선이 있고, 왼쪽에는 i-1번 풍선이 있다. 단, 1번 풍선의 왼쪽에 N번 풍선이 있고, N번 풍선의 오른쪽에 1번 풍선이 있다. 각 풍선 안에는 종이가 하나 들어있고, 종이에는 -N보다 크거나 같고, N보다 작거나 같은 정수가 하나 적혀있다. 이 풍선들을 다음과 같은 규칙으로 터뜨린다. 우선, 제일 처음에는 1번 풍선을 터뜨린다. 다음에는 풍선 안에 있는 종이를 꺼내어 그 종이에 적혀있는 값만큼 이동하여 다음 풍선을 터뜨린다. 양수가 적혀 있을 경우에는 오른쪽으로, 음수가 적혀 있을 때는 왼쪽으로 이동한다. 이동할 때에는 이미 터진 풍선은 빼고 이동한다. 예를 들어 다섯 개의 풍선 안에 차례로 3, 2, 1, -3, -1이 적혀 있었다고 하자. 이 경우 3이 적혀 있는 1번 풍선, -3이 적혀 있는 4번 풍선, -1이 적혀 있는 5번 풍선, 1이 적혀 있는 3번 풍선, 2가 적혀 있는 2번 풍선의 순서대로 터지게 된다.
입력
첫째 줄에 자연수 N(1 ≤ N ≤ 1,000)이 주어진다. 다음 줄에는 차례로 각 풍선 안의 종이에 적혀 있는 수가 주어진다. 종이에 0은 적혀있지 않다.
출력
첫째 줄에 터진 풍선의 번호를 차례로 나열한다.
문제 풀이 1 (Map을 이용 180ms)
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) throws IOException {
StringBuilder sb = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
Deque<Map<Integer, Integer>> deque = new ArrayDeque<>();
int forCount = Integer.parseInt(br.readLine());
StringTokenizer st = new StringTokenizer(br.readLine());
for (int i = 1; i <= forCount; i++) {
Map<Integer, Integer> map = new HashMap<>();
int value = Integer.parseInt(st.nextToken());
map.put(i, value);
deque.addLast(map);
}
Map<Integer, Integer> remove = deque.removeFirst();
Integer value = remove.get(1);
sb.append(1).append(" ");
while (!deque.isEmpty()) {
if (value > 0) {
for (int i = 0; i < value - 1; i++) deque.addLast(deque.removeFirst());
remove = deque.removeFirst();
} else {
for (int i = 0; i < Math.abs(value) - 1; i++) deque.addFirst(deque.removeLast());
remove = deque.removeLast();
}
Integer index = new ArrayList<>(remove.keySet()).get(0);
sb.append(index).append(" ");
value = remove.get(index);
}
bw.write(sb.toString());
bw.flush();
bw.close();
br.close();
}
}
문제 풀이2 (int[] 을 이용 152ms)
import java.io.*;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
StringBuilder sb = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
Deque<int[]> deque = new ArrayDeque<>();
int forCount = Integer.parseInt(br.readLine());
StringTokenizer st = new StringTokenizer(br.readLine());
for (int i = 1; i <= forCount; i++) {
int value = Integer.parseInt(st.nextToken());
deque.addLast(new int[]{i, value});
}
int[] removeArray = deque.removeFirst();
int value = removeArray[1];
sb.append(removeArray[0]).append(" ");
while (!deque.isEmpty()) {
if (value > 0) {
for (int i = 0; i < value - 1; i++) deque.addLast(deque.removeFirst());
removeArray = deque.removeFirst();
} else {
for (int i = 0; i < (-value) - 1; i++) deque.addFirst(deque.removeLast());
removeArray = deque.removeLast();
}
value = removeArray[1];
sb.append(removeArray[0]).append(" ");
}
bw.write(sb.toString());
bw.flush();
bw.close();
br.close();
}
}
-
[baekjoon] 도키도키 간식드리미 (백준 12789 java 풀이)
intro : ArrayDeque를 두개 사용하여 문제를 풀어가는게 포인트.
백준 문제링크
문제
인하대학교 학생회에서는 중간, 기말고사 때마다 시험 공부에 지친 학우들을 위해 간식을 나눠주는 간식 드리미 행사를 실시한다. 승환이는 시험 기간이 될 때마다 간식을 받을 생각에 두근두근 설레서 시험 공부에 집중을 못 한다. 이번 중간고사에서도 역시 승환이는 설레는 가슴을 안고 간식을 받기 위해 미리 공지된 장소에 시간 맞춰 도착했다. 그런데 이게 무슨 날벼락인가! 그 곳에는 이미 모든 학생들이 모여있었고, 승환이는 마지막 번호표를 받게 되었다. 설상가상으로 몇몇 양심에 털이 난 학생들이 새치기를 거듭한 끝에 대기열의 순서마저 엉망이 되고 말았다. 간식을 나눠주고 있던 인규는 학우들의 터져 나오는 불만에 번호표 순서로만 간식을 줄 수 있다고 말했다. 그제야 학생들이 순서대로 줄을 서려고 했지만 공간이 너무 협소해서 마음대로 이동할 수 없었다. 다행히도 대기열의 왼쪽에는 1열로 설 수 있는 공간이 존재하여 이 공간을 잘 이용하면 모두가 순서대로 간식을 받을 수 있을지도 모른다. 자칫 간식을 못 받게 될지도 모른다는 위기감을 느낀 승환이는 자신의 컴퓨터 알고리즘적 지식을 활용해 과연 모든 사람들이 순서대로 간식을 받을 수 있는지 확인하는 프로그램을 만들기로 했다. 만약 불가능 하다면 승환이는 이번 중간고사를 망치게 될 것 이고 가능하다면 힘을 얻어 중간고사를 잘 볼 수 있을지도 모른다. 사람들은 현재 1열로 줄을 서있고, 맨 앞의 사람만 이동이 가능하다. 인규는 번호표 순서대로만 통과할 수 있는 라인을 만들어 두었다. 이 라인과 대기열의 맨 앞 사람 사이에는 한 사람씩 1열이 들어갈 수 있는 공간이 있다. 현재 대기열의 사람들은 이 공간으로 올 수 있지만 반대는 불가능하다. 승환이를 도와 프로그램을 완성하라. 현재 간식 배부 공간을 그림으로 나타내면 다음과 같다.
위 예제는 다음 그림과 같이 움직였을 때 모두가 순서대로 간식을 받을 수 있다.
입력
입력의 첫째 줄에는 현재 승환이의 앞에 서 있는 학생들의 수 N(1 ≤ N ≤ 1,000,자연수)이 주어진다. 다음 줄에는 승환이 앞에 서있는 모든 학생들의 번호표(1,2,…,N) 순서가 앞에서부터 뒤 순서로 주어진다.
출력
승환이가 무사히 간식을 받을 수 있으면 “Nice”(따옴표는 제외)를 출력하고 그렇지 않다면 “Sad”(따옴표는 제외)를 출력한다.
문제 풀이 (틀린 방법)
위 코드에서 틀린 부분은 deque2에서 연속되어 stand와 일치하는값이 나올 수 있다는 경우의 수를 생각하지 못해서 틀린 로직을 구성하게 되었다. while문으로 stand값이 일치하는값이 존재한다면 remove를 해주어야 하는데 그부분을 놓쳤다.
import java.io.*;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
Deque<Integer> deque1 = new ArrayDeque<>();
Deque<Integer> deque2 = new ArrayDeque<>();
br.readLine();
StringTokenizer st = new StringTokenizer(br.readLine());
while (st.hasMoreTokens()) deque1.add(Integer.parseInt(st.nextToken()));
int stand = 1;
while (!deque1.isEmpty()) {
Integer value = deque1.removeFirst();
if (value == stand) stand++;
else {
if (!deque2.isEmpty()) {
if (deque2.peekFirst() == stand) {
deque2.removeFirst();
stand++;
} else deque2.addFirst(value);
} else deque2.addFirst(value);
}
}
for (Integer temp : deque2) {
if (temp == stand) {
deque2.removeFirst();
stand++;
} else break;
}
if (deque2.isEmpty()) bw.write("Nice");
else bw.write("Sad");
bw.flush();
bw.close();
br.close();
}
}
문제 풀이 (112ms)
import java.io.*;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
Deque<Integer> deque1 = new ArrayDeque<>();
Deque<Integer> deque2 = new ArrayDeque<>();
br.readLine();
StringTokenizer st = new StringTokenizer(br.readLine());
while (st.hasMoreTokens()) deque1.add(Integer.parseInt(st.nextToken()));
int stand = 1;
while (!deque1.isEmpty()) {
Integer value = deque1.removeFirst();
if (value == stand) {
stand++;
while (!deque2.isEmpty() && (deque2.peekFirst() == stand)) {
deque2.removeFirst();
stand++;
}
} else deque2.addFirst(value);
}
if (deque2.isEmpty()) bw.write("Nice");
else bw.write("Sad");
bw.flush();
bw.close();
br.close();
}
}
문제 해석
위 문제는 큐 두개를 사용해서 풀이를 진행 할 수 있다. 주어진 예시를 잘 보면 1번 큐에서 2번 큐로의 값 이동은 가능하지만, 2번큐에서 1번 큐로의 이동은 불가하다. 그말은 1번큐에서의 값을 꺼내서 stand와 값이 일치하는지 확인하고, 일치한다면 remove 를 하고 혹시 2번 큐에서 다음 stand 값과 일치하는 값이 존재하는지 확인한다. 연속된 일치하는 값이 존재할 수 있으므로 while 문으로 반복하여 확인한다. 사실 이 문제에서 가장 중요한 부분은 언제 1번 큐에서 2번 큐로의 값을 이동시킬것인지와, 2번큐의 값을 언제 꺼내서 확인할 것인지 로직을 구성하는게 포인트가 되는 것 같다.
-
[baekjoon] 대표값2 (백준 2587 java 풀이)
intro : Arrays.sort 메소드를 통해 오름차순으로 쉽게 정렬할 수 있다.
백준 문제링크
문제
어떤 수들이 있을 때, 그 수들을 대표하는 값으로 가장 흔하게 쓰이는 것은 평균이다. 평균은 주어진 모든 수의 합을 수의 개수로 나눈 것이다. 예를 들어 10, 40, 30, 60, 30의 평균은 (10 + 40 + 30 + 60 + 30) / 5 = 170 / 5 = 34가 된다. 평균 이외의 또 다른 대표값으로 중앙값이라는 것이 있다. 중앙값은 주어진 수를 크기 순서대로 늘어 놓았을 때 가장 중앙에 놓인 값이다. 예를 들어 10, 40, 30, 60, 30의 경우, 크기 순서대로 늘어 놓으면 10 30 30 40 60이 되고 따라서 중앙값은 30이 된다. 다섯 개의 자연수가 주어질 때 이들의 평균과 중앙값을 구하는 프로그램을 작성하시오.
입력
첫째 줄부터 다섯 번째 줄까지 한 줄에 하나씩 자연수가 주어진다. 주어지는 자연수는 100 보다 작은 10의 배수이다.
출력
첫째 줄에는 평균을 출력하고, 둘째 줄에는 중앙값을 출력한다. 평균과 중앙값은 모두 자연수이다.
문제 풀이 (100ms)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int[] arr = new int[5];
int sum = 0;
int avg = 0;
for (int i = 0; i < 5; i++) {
int value = Integer.parseInt(br.readLine());
sum += value;
arr[i] = value;
}
avg = sum / 5;
Arrays.sort(arr);
System.out.println(avg);
System.out.println(arr[2]);
br.close();
}
}
-
-
[baekjoon] 영화감독 숌 (백준 1436 java 풀이)
intro : 숫자가 666을 포함하는지 확인하는게 포인트
백준 문제링크
문제
666은 종말을 나타내는 수라고 한다. 따라서, 많은 블록버스터 영화에서는 666이 들어간 제목을 많이 사용한다. 영화감독 숌은 세상의 종말 이라는 시리즈 영화의 감독이다. 조지 루카스는 스타워즈를 만들 때, 스타워즈 1, 스타워즈 2, 스타워즈 3, 스타워즈 4, 스타워즈 5, 스타워즈 6과 같이 이름을 지었고, 피터 잭슨은 반지의 제왕을 만들 때, 반지의 제왕 1, 반지의 제왕 2, 반지의 제왕 3과 같이 영화 제목을 지었다. 하지만 숌은 자신이 조지 루카스와 피터 잭슨을 뛰어넘는다는 것을 보여주기 위해서 영화 제목을 좀 다르게 만들기로 했다. 종말의 수란 어떤 수에 6이 적어도 3개 이상 연속으로 들어가는 수를 말한다. 제일 작은 종말의 수는 666이고, 그 다음으로 큰 수는 1666, 2666, 3666, …. 이다. 따라서, 숌은 첫 번째 영화의 제목은 “세상의 종말 666”, 두 번째 영화의 제목은 “세상의 종말 1666”와 같이 이름을 지을 것이다. 일반화해서 생각하면, N번째 영화의 제목은 세상의 종말 (N번째로 작은 종말의 수) 와 같다. 숌이 만든 N번째 영화의 제목에 들어간 수를 출력하는 프로그램을 작성하시오. 숌은 이 시리즈를 항상 차례대로 만들고, 다른 영화는 만들지 않는다.
입력
첫째 줄에 N이 주어진다. N은 10,000보다 작거나 같은 자연수이다.
출력
첫째 줄에 N번째 영화의 제목에 들어간 수를 출력한다.
문제 풀이 (264ms)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int value = Integer.parseInt(br.readLine());
int count = 0;
int index = 666;
while (value != count) {
if (Integer.toString(index).contains("666")) {
count++;
}
index++;
}
System.out.println(index - 1);
}
}
-
-
-
-
[baekjoon] 알고리즘 수업 - 알고리즘의 수행 시간 6 (백준 24267 java 풀이)
intro : 순열과 조합을 떠올려야 하는 문제인지는 몰랐다.
백준 문제링크
문제
오늘도 서준이는 알고리즘의 수행시간 수업 조교를 하고 있다. 아빠가 수업한 내용을 학생들이 잘 이해했는지 문제를 통해서 확인해보자. 입력의 크기 n이 주어지면 MenOfPassion 알고리즘 수행 시간을 예제 출력과 같은 방식으로 출력해보자.
MenOfPassion 알고리즘은 다음과 같다.
MenOfPassion(A[], n) {
sum <- 0;
for i <- 1 to n - 2
for j <- i + 1 to n - 1
for k <- j + 1 to n
sum <- sum + A[i] × A[j] × A[k]; # 코드1
return sum;
}
입력
첫째 줄에 입력의 크기 n(1 ≤ n ≤ 500,000)이 주어진다.
출력
첫째 줄에 코드1 의 수행 횟수를 출력한다. 둘째 줄에 코드1의 수행 횟수를 다항식으로 나타내었을 때, 최고차항의 차수를 출력한다. 단, 다항식으로 나타낼 수 없거나 최고차항의 차수가 3보다 크면 4를 출력한다.
문제 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
long n = Long.parseLong(br.readLine());
System.out.println((n - 2) * (n - 1) * (n) / 6);
System.out.println(3);
}
}
문제 해석
MenOfPassion 알고리즘을 코드로 표현하면 다음과 같다.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
long n = Long.parseLong(br.readLine());
int count = 0;
for (int i = 1; i <= n - 2; i++) {
for (int j = i + 1; j <= n - 1; j++) {
for (int k = j + 1; k <= n; k++) {
count++;
System.out.println(i + "," + j + "," + k + ",");
}
System.out.println();
}
System.out.println();
}
System.out.println("count = " + count);
}
}
출력결과도 같이 한번 보자.
7
1,2,3,
1,2,4,
1,2,5,
1,2,6,
1,2,7,
1,3,4,
1,3,5,
1,3,6,
1,3,7,
1,4,5,
1,4,6,
1,4,7,
1,5,6,
1,5,7,
1,6,7,
2,3,4,
2,3,5,
2,3,6,
2,3,7,
2,4,5,
2,4,6,
2,4,7,
2,5,6,
2,5,7,
2,6,7,
3,4,5,
3,4,6,
3,4,7,
3,5,6,
3,5,7,
3,6,7,
4,5,6,
4,5,7,
4,6,7,
5,6,7,
count = 35
출력결과를 보니 무언가 떠오르지 않는가? 이건 순열과 조합에서 조합의 경우의 수를 보는 것과 같다. 조합의 경우의 수를 구하는 공식은 다음과 같다.
예제에서 7 이 입력값으로 주어졌을때, 35가 출력값으로 나온 이유는 다음과 같은 계산 과정을 통해 도출 할 수 있다.
계산 과정1
(7 * 6 * 5 * 4 * 3 * 2 * 1)
(4 * 3 * 2 * 1) * (3 * 2 * 1)
계산 과정2
(7 * 6 * 5)
(3 * 2 * 1)
계산 과정3
35
위 과정을 공식으로 도출하면 다음과 같다.
(n * n -1 * n - 2) / 6
여기서 6으로 나누는 이유가 궁금할 수 있는데, 문제에서 주어진 반복문의 depts는 총 3회이다. 결국 7개의 숫자 중에서 3개를 뽑는 조합의 경우의 수를 구하는 조건으로 한정시켜 바라볼 수 있기에 6으로 나눈다.
-
-
[baekjoon] 알고리즘 수업 - 알고리즘의 수행 시간 4 (백준 24265 java 풀이)
intro : 1 ~ N 까지의 합을 구하는 공식은 N(N+1)/2 이다.
백준 문제링크
문제
오늘도 서준이는 알고리즘의 수행시간 수업 조교를 하고 있다. 아빠가 수업한 내용을 학생들이 잘 이해했는지 문제를 통해서 확인해보자. 입력의 크기 n이 주어지면 MenOfPassion 알고리즘 수행 시간을 예제 출력과 같은 방식으로 출력해보자.
MenOfPassion 알고리즘은 다음과 같다.
MenOfPassion(A[], n) {
sum <- 0;
for i <- 1 to n - 1
for j <- i + 1 to n
sum <- sum + A[i] × A[j]; # 코드1
return sum;
}
입력
첫째 줄에 입력의 크기 n(1 ≤ n ≤ 500,000)이 주어진다.
출력
첫째 줄에 코드1 의 수행 횟수를 출력한다. 둘째 줄에 코드1의 수행 횟수를 다항식으로 나타내었을 때, 최고차항의 차수를 출력한다. 단, 다항식으로 나타낼 수 없거나 최고차항의 차수가 3보다 크면 4를 출력한다.
문제 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
long n = Long.parseLong(br.readLine());
System.out.println((n - 1) * n / 2);
System.out.println(2);
}
}
문제 해석
위 문제에서 주어진 MenOfPassion 을 코드로 나타내면 다음과 같다.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
long n = Long.parseLong(br.readLine());
int count = 0;
for (int i = 1; i <= n - 1; i++) {
for (int j = i + 1; j <= n; j++) {
System.out.print(i + "," + j + " ");
count++;
}
System.out.println();
}
System.out.println("count = " + count);
}
}
출력결과도 같이 눈으로 보도록 하겠다.
7
1,2 1,3 1,4 1,5 1,6 1,7
2,3 2,4 2,5 2,6 2,7
3,4 3,5 3,6 3,7
4,5 4,6 4,7
5,6 5,7
6,7
count = 21
결국 우리가 구해야하는 반복횟수는 입력값이 7인 경우 6 + 5 + 4 + 3 + 2 + 1 인 값이다. 이 합 공식은 다음과 같은 공식과 굉장히 유사하다.
1 부터 10 까지의 합을 구하는 공식
공식 : n(n+1)/2
결과 : 55
위 공식을 응용한다면, 반복횟수의 합은 1부터 n-1 까지의 합을 구하는 것과 같기에 (n-1)*n/2 의 다항식을 도출할 수 있다.
-
-
-
-
[baekjoon] 요세푸스 문제 0 (백준 11866 java 풀이)
intro : 문제를 이해하는게 코드를 짜는거보다 어려운거 같다. 문제를 쉽게 해석하자면 앞에있는값을 뒤로 보내고 K번째 값을 순차적으로 뺴는 문제이다.
백준 문제링크
문제
요세푸스 문제는 다음과 같다. 1번부터 N번까지 N명의 사람이 원을 이루면서 앉아있고, 양의 정수 K(≤ N)가 주어진다. 이제 순서대로 K번째 사람을 제거한다. 한 사람이 제거되면 남은 사람들로 이루어진 원을 따라 이 과정을 계속해 나간다. 이 과정은 N명의 사람이 모두 제거될 때까지 계속된다. 원에서 사람들이 제거되는 순서를 (N, K)-요세푸스 순열이라고 한다. 예를 들어 (7, 3)-요세푸스 순열은 <3, 6, 2, 7, 5, 1, 4> 이다. N과 K가 주어지면 (N, K)-요세푸스 순열을 구하는 프로그램을 작성하시오.
입력
첫째 줄에 N과 K가 빈 칸을 사이에 두고 순서대로 주어진다. (1 ≤ K ≤ N ≤ 1,000)
출력
예제와 같이 요세푸스 순열을 출력한다.
문제 풀이 (160ms)
import java.io.*;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
StringBuilder sb = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
Deque<Integer> deque = new ArrayDeque<>();
StringTokenizer st = new StringTokenizer(br.readLine());
int forCount1 = Integer.parseInt(st.nextToken());
int forCount2 = Integer.parseInt(st.nextToken());
for (int i = 1; i <= forCount1; i++) deque.addLast(i);
while (!deque.isEmpty()) {
for (int i = 0; i < forCount2 - 1; i++) {
Integer value = deque.removeFirst();
deque.addLast(value);
}
sb.append(deque.removeFirst()).append(", ");
}
bw.write("<" + sb.substring(0, sb.toString().length() - 2) + ">");
bw.flush();
bw.close();
}
}
-
[baekjoon] 균형잡힌 세상 (백준 4949 java 풀이)
intro : 문제에서 주어진 조건 짝을 이루는 두 괄호가 있을 때, 그 사이에 있는 문자열도 균형이 잡혀야 한다. 이 말은 [ 괄호 다음에 ( , ) 가 나오면 안되고 ] 가 나와야 함을 뜻한다.
올바른 경우: [ ( ) ] → 여기서 [ ] 안에 ( )가 있으므로 괄호들이 짝을 이루고 있다.
잘못된 경우: [ ( ] ) → [ 와 ] 사이에 ( 와 )가 있는 것은 유효하지 않으므로 잘못된 괄호 쌍이다.
백준 문제링크
문제
세계는 균형이 잘 잡혀있어야 한다. 양과 음, 빛과 어둠 그리고 왼쪽 괄호와 오른쪽 괄호처럼 말이다. 정민이의 임무는 어떤 문자열이 주어졌을 때, 괄호들의 균형이 잘 맞춰져 있는지 판단하는 프로그램을 짜는 것이다. 문자열에 포함되는 괄호는 소괄호(“()”) 와 대괄호(“[]”)로 2종류이고, 문자열이 균형을 이루는 조건은 아래와 같다. 모든 왼쪽 소괄호(“(“)는 오른쪽 소괄호(“)”)와만 짝을 이뤄야 한다. 모든 왼쪽 대괄호(“[“)는 오른쪽 대괄호(“]”)와만 짝을 이뤄야 한다. 모든 오른쪽 괄호들은 자신과 짝을 이룰 수 있는 왼쪽 괄호가 존재한다. 모든 괄호들의 짝은 1:1 매칭만 가능하다. 즉, 괄호 하나가 둘 이상의 괄호와 짝지어지지 않는다. 짝을 이루는 두 괄호가 있을 때, 그 사이에 있는 문자열도 균형이 잡혀야 한다. 정민이를 도와 문자열이 주어졌을 때 균형잡힌 문자열인지 아닌지를 판단해보자.
입력
각 문자열은 마지막 글자를 제외하고 영문 알파벳, 공백, 소괄호(“( )”), 대괄호(“[ ]”)로 이루어져 있으며, 온점(“.”)으로 끝나고, 길이는 100글자보다 작거나 같다. 입력의 종료조건으로 맨 마지막에 온점 하나(“.”)가 들어온다.
출력
각 줄마다 해당 문자열이 균형을 이루고 있으면 “yes”를, 아니면 “no”를 출력한다.
문제 풀이 (184ms)
import java.io.*;
import java.util.ArrayDeque;
import java.util.Deque;
public class Main {
public static void main(String[] args) throws IOException {
StringBuilder sb = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
Deque<<haracter> deque = new ArrayDeque<>();
String input;
while (!(input = br.readLine()).equals(".")) {
for (int i = 0; i < input.length() - 1; i++) {
char ch = input.charAt(i);
if (ch == '[' || ch == '(') {
deque.addLast(ch);
} else if (ch == ']' || ch == ')') {
if (deque.isEmpty()) {
deque.addLast(ch);
break;
} else {
Character peekLast = deque.peekLast();
if (isMatch(peekLast, ch)) deque.removeLast();
else {
deque.addLast(ch);
break;
}
}
}
}
if (deque.isEmpty()) sb.append("yes").append("\n");
else sb.append("no").append("\n");
deque.clear();
}
bw.write(sb.toString());
bw.flush();
bw.close();
}
public static boolean isMatch(char open, char close) {
return (open == '[' && close == ']') || (open == '(' && close == ')');
}
}
-
-
-
-
[java] 연산자
intro : java 연산자에 대한 개념을 알아보자.
연산자
자바에는 산술연산자, 증감 연산자, 비교 연산자, 논리 연산자, 대입 연산자, 삼항 연산자 등과 같은 다양한 연산자가 존재한다. 기본적으로 연산자와 피연산자의 개념은 다음과 같다.
3 + 4 // 연산자(operator): 연산 기호 - `+`, `-`
a + b // 피연산자(operand): 연산 대상 - `3`, `4`, `a`, `b`
산술 연산자
산술 연산자는 주로 숫자를 계산하는 데 사용된다. 우리가 이미 잘 알고 있는 수학 연산을 수행한다. 산술연산자의 종류는 더하기 (+) 빼기 (-) 곱하기(*) 나누기(/) 나머지(%) 가 있다.
public class Operator1 {
public static void main(String[] args) {
// 변수 초기화
int a = 5;
int b = 2;
// 덧셈
int sum = a + b;
System.out.println("a + b = " + sum); // 출력: a + b = 7
// 뺄셈
int diff = a - b;
System.out.println("a - b = " + diff); // 출력: a - b = 3
// 곱셈
int multi = a * b;
System.out.println("a * b = " + multi); // 출력: a * b = 10
// 나눗셈
int div = a / b;
System.out.println("a / b = " + div); // 출력: a / b = 2
// 나머지
int mod = a % b;
System.out.println("a % b = " + mod); // 출력: a % b = 1
}
}
주의! 0 으로 나누기
10 / 0 과 같이 숫자는 0으로 나눌 수 없다. 이 경우 프로그램에 다음과 같은 오류가 발생한다. (Exception in thread "main" java.lang.ArithmeticException: / by zero)
문자열 더하기
자바는 특별하게도 문자열에도 + 연산자를 사용할 수 있다. 문자열에 + 연산자를 사용하면 두 문자를 연결 할 수 있다.
public class Operator2 {
public static void main(String[] args) {
//문자열과 문자열 더하기1
String result1 = "hello " + "world";
System.out.println(result1);
//문자열과 문자열 더하기2
String s1 = "string1";
String s2 = "string2";
String result2 = s1 + s2;
System.out.println(result2);
//문자열과 숫자 더하기1
String result3 = "a + b = " + 10;
System.out.println(result3);
//문자열과 숫자 더하기2
int num = 20;
String str = "a + b = ";
String result4 = str + num;
System.out.println(result4);
}
}
주의! 문자열 + 숫자
자바에서 문자와 숫자를 더하면 숫자를 문자열로 변경한 다음에 서로 더한다. 아래 코드의 결과는 20을 예상할 수도 있지만, 실제로는 1010 값이 출력된다. 쉽게 얘기해서 문자열과 더하는 모든 것들은 전부 문자열이 된다.
public class Main {
public static void main(String[] args) {
//문자열과 문자열 더하기1
String result1 = "hello " + "world";
System.out.println(result1);
String str = "10";
int a = 10;
System.out.println(str + a); // 1010 출력
}
}
연산자 우선순위
자바는 다음과 같은 연산자 우선순위가 있다. 높은 것에서 낮은 순으로 적용된다. 처음에 나오는 괄호가 우선순위가 가장 높고, 대입 연산자가 우선순위가 가장 낮다.
우선순위
연산자 유형
연산자
1
괄호
()
2
단항 연산자
++,--,!,~,new,(type)
3
산술 연산자
*, /, %, +, -
4
Shift 연산자
<<, >>, >>>
5
비교 연산자
<,<=,>,>=,instanceof
6
등식 연산자
==, !=
7
비트 연산자
&, ^, |
8
논리 연산자
&&, ||
9
삼항 연산자
? :
10
대입 연산자
=, +=, -=, *=, /=, %= 등
잘 모르는 ~ 연산자
자바에서 ~ 연산자는 비트 반전 연산자(bitwise NOT)입니다. 이 연산자는 피연산자의 모든 비트를 반전시키는 데 사용됩니다. 즉, 0을 1로, 1을 0으로 바꿉니다. 이걸 응용하면, 음수를 비트로 표현하는 방법에 대해서도 알 수 있습니다. -5를 표현하는 방법은 5의 보수 즉 ~5 을 한뒤 +1 을 해주는 것으로 음수를 2진법으로 표현할 수 있습니다.
public class Main {
public static void main(String[] args) {
int a = 5; // 이진수: 00000000 00000000 00000000 00000101
int result = ~a;
System.out.println(result); // 출력: -6
}
}
잘 모르는 Shift 연산자
1. << 왼쪽 쉬프트 연산자
비트를 왼쪽으로 이동시키고, 오른쪽에 0을 채웁니다.
예시: a << n은 a를 왼쪽으로 n비트만큼 이동시킵니다.
int a = 1; // 이진수: 00000001
int result = a << 1; // 왼쪽으로 1비트 이동
System.out.println(result); // 출력: 2 (이진수: 00000010)
2. >> 오른쪽 쉬프트 연산자
비트를 오른쪽으로 이동시키고, 왼쪽에 부호 비트를 채웁니다. (부호 비트는 양수인 경우 0, 음수인 경우 1)
a >> n은 a를 오른쪽으로 n비트만큼 이동시키며, 부호 비트(1 또는 0)로 채웁니다.
int a = 4; // 이진수: 00000100
int result = a >> 1; // 오른쪽으로 1비트 이동
System.out.println(result); // 출력: 2 (이진수: 00000010)
3. >>> 무조건 오른쪽 쉬프트 연산자 (<<< 무조건 왼쪽 쉬프트 연산자는 없습니다.)
비트를 오른쪽으로 이동시키고, 왼쪽에 항상 0을 채웁니다. 부호 비트와 상관없이 0으로 채워집니다.
예시: a >>> n은 a를 오른쪽으로 n비트만큼 이동시키며, 빈 자리는 항상 0으로 채웁니다.
int a = -5; // 이진수: 11111111111111111111111111111011
int result = a >>> 1; // 오른쪽으로 1비트 이동
System.out.println(result); // 출력: 2147483642
잘 모르는 비트 연산자
1. & 연산자
&는 비트 단위 AND 연산자로, 두 비트가 모두 1일 때만 결과가 1이 됩니다. 그 외의 경우에는 결과가 0이 됩니다.
public class Main {
public static void main(String[] args) {
int a = 5; // 이진수: 0101
int b = 3; // 이진수: 0011
int result = a & b;
System.out.println(result); // 출력: 1
}
}
2. ^ (비트 XOR) 연산자
^는 비트 단위 XOR 연산자로, 두 비트가 서로 다를 때만 결과가 1이 됩니다. 같은 비트는 0이 됩니다.
public class Main {
public static void main(String[] args) {
int a = 5; // 이진수: 0101
int b = 3; // 이진수: 0011
int result = a ^ b;
System.out.println(result); // 출력: 6
}
}
3. | (비트 OR) 연산자
|는 비트 단위 OR 연산자로, 두 비트 중 하나라도 1이면 결과가 1이 됩니다. 두 비트가 모두 0일 때만 결과가 0입니다.
public class Main {
public static void main(String[] args) {
int a = 5; // 이진수: 0101
int b = 3; // 이진수: 0011
int result = a | b;
System.out.println(result); // 출력: 7
}
}
증감 연산자
증가 및 감소 연산자를 줄여서 증감 연산자라 한다. 증감 연산자는 ++ 와 -- 로 표현되며, 이들은 변수의 값을 1만큼 증가시키거나 감소시킨다. 프로그래밍에서는 값을 1씩 증가하거나 1씩 감소할 때가 아주 많기 때문에 이런 편의 기능을 제공한다.
public class OperatorAdd1 {
public static void main(String[] args) {
int a = 0;
a = a + 1;
System.out.println("a = " + a); //1
a = a + 1;
System.out.println("a = " + a); //2
//증감 연산자
++a; //a = a + 1
System.out.println("a = " + a); //3
++a; //a = a + 1
System.out.println("a = " + a); //4
}
}
전위, 후위 증감연산자
증감 연산자는 피연산자 앞에 두거나 뒤에 둘 수 있으며, 연산자의 위치에 따라 연산이 수행되는 시점이 달라진다. ++a : 증감 연산자를 피연산자 앞에 둘 수 있다. 이것을 앞에 있다고 해서 전위(Prefix) 증감 연산자라 한다. a++ : 증감 연산자를 피연산자 뒤에 둘 수 있다. 이것을 뒤에 있다고 해서 후위(Postfix) 증감 연산자라 한다.
public class OperatorAdd2 {
public static void main(String[] args) {
// 전위 증감 연산자 사용 예
int a = 1;
int b = 0;
b = ++a; // a의 값을 먼저 증가시키고, 그 결과를 b에 대입
System.out.println("a = " + a + ", b = " + b); // 결과: a = 2, b = 2
// 후위 증감 연산자 사용 예
a = 1; // a 값을 다시 1로 지정
b = 0; // b 값을 다시 0으로 지정
b = a++; // a의 현재 값을 b에 먼저 대입하고, 그 후 a 값을 증가시킴
System.out.println("a = " + a + ", b = " + b); // 결과: a = 2, b = 1
}
}
비교 연산자
비교 연산자는 두 값을 비교하는 데 사용한다. 비교 연산자는 주로 뒤에서 설명하는 조건문과 함께 사용한다. 비교 연산자를 사용하면 참(true) 또는 거짓(false)이라는 결과가 나온다. 참 거짓은 boolean 형을 사용한다. 여기서 주의할 점은 = 와 == (= x2)이 다르다는 점이다.= : 대입 연산자, 변수에 값을 대입한다. == : 동등한지 확인하는 비교 연산자
public class Comp1 {
public static void main(String[] args) {
int a = 2;
int b = 3;
System.out.println(a == b); // false, a와 b는 같지 않다
System.out.println(a != b); // true, a와 b는 다르다
System.out.println(a > b); // false, a는 b보다 크지 않다
System.out.println(a < b); // true, a는 b보다 작다
System.out.println(a >= b); // false, a는 b보다 크거나 같지 않다
System.out.println(a <= b); // true, a는 b보다 작거나 같다
//결과를 boolean 변수에 담기
boolean result = a == b; //a == b: false
System.out.println(result); //false
}
}
문자열 비교
문자열 비교는 객체이기 때문에 == 으로 비교할수 없다. 문자열 String 클래스가 제공하는 equals 메소드를 통해 비교해야한다.
논리 연산자
논리 연산자는 boolean 형인 true , false 를 비교하는데 사용한다. && (그리고) : 두 피연산자가 모두 참이면 참을 반환, 둘중 하나라도 거짓이면 거짓을 반환 || (또는) : 두 피연산자 중 하나라도 참이면 참을 반환, 둘다 거짓이면 거짓을 반환 ! (부정) : 피연산자의 논리적 부정을 반환. 즉, 참이면 거짓을, 거짓이면 참을 반환
public class Logical1 {
public static void main(String[] args) {
System.out.println("&&: AND 연산");
System.out.println(true && true); //true
System.out.println(true && false);//false
System.out.println(false && false);//false
System.out.println("||: OR 연산");
System.out.println(true || true); //true
System.out.println(true || false);//true
System.out.println(false || false);//false
System.out.println("! 연산");
System.out.println(!true); //false
System.out.println(!false); //true
System.out.println("변수 활용");
boolean a = true;
boolean b = false;
System.out.println(a && b); // false
System.out.println(a || b); // true
System.out.println(!a); // false
System.out.println(!b); // true
}
}
대입 연산자
대입 연산자(=)는 값을 변수에 할당하는 연산자다. 이 연산자를 사용하면 변수에 값을 할당할 수 있다. 예를 들어, int a = 1 는 a 라는 변수에 1 이라는 값을 할당한다. 산술 연산자와 대입 연산자를 한번에 축약해서 사용할 수 있는데, 이것을 축약(복합) 대입 연산자라 한다. (연산자 종류: +=,-=,*=,/=,%=)
public class Assign1 {
public static void main(String[] args) {
int a = 5; // 5
a += 3; // 8 (5 + 3): a = a + 3
a -= 2; // 6 (8 - 2): a = a - 2
a *= 4; // 24 (6 * 4): a = a * 4
a /= 3; // 8 (24 / 3): a = a / 3
a %= 5; // 3 (8 % 5) : a = a % 5
System.out.println(a);
}
}
-
[design-pattern] facade 패턴
intro : facade 패턴에 대해서 알아보자
Facade 패턴이란 ?
Facade 란 프랑스어로 어떤 건물의 외벽을 뜻하는 말이다. 외벽을 뜻하는 단어로 이루어진 디자인 패턴은 어떤 패턴일까 ? 좀 더 자세히 알아보자면, 구조적 디자인 패턴 중 하나로 복잡한 서브시스템을 단순화하여 사용하는 패턴이라고 볼 수 있다. 이 패턴은 클라이언트가 복잡한 시스템이나 여러 클래스를 직접 다루지 않고, 단일 인터페이스를 통해 시스템을 간단히 사용할 수 있도록 해준다. 즉, 복잡한 내부 시스템을 숨기고, 간단하고 일관된 인터페이스를 제공하는 것이 Facade 패턴의 핵심이라고 볼 수 있다.
Facade 패턴은 언제 사용하는가 ?
Facade 패턴은 복잡한 서브시스템의 인터페이스를 단순화하여 클라이언트가 쉽게 접근할 수 있도록 할 때 사용한다. 이 패턴은 클라이언트와 복잡한 서브시스템 간에 하나의 통합된 진입점을 제공함이 중요한 특징이다.
Facade 패턴 적용 전 (1)
어떠한 클라이언트는 아침에 일어나면 온도를 조절하고, 불을 켜고, 커피를 마신다. 이러한 행동은 다음과 같이 각 클래스의 메소드들로 나누어져 있으며, 객체를 생성하여 각각 호출하여야 한다.
public class Thermostat {
public void setTemperature(int temperature) {
System.out.println("Setting thermostat to " + temperature + " degrees.");
}
}
public class Lights {
public void on() {
System.out.println("Lights are on.");
}
public void off() {
System.out.println("Lights are off.");
}
}
public class CoffeeMaker {
public void brewCoffee() {
System.out.println("Brewing coffee.");
}
}
실제로 클라이언트는 각 클래스의 메소드들을 어떻게 호출해야 하는지 다음과 같은 코드를 보면 알 수 있다.
// Client code
public class Main {
public static void main(String[] args) {
Thermostat thermostat = new Thermostat(); // 온도 class 객체 생성
Lights lights = new Lights(); // 불 class 객체 생성
CoffeeMaker coffeeMaker = new CoffeeMaker(); // 커피 머신 class 객체 생성
thermostat.setTemperature(25); // 온도 25도 설정
lights.on(); // 불 전원 on
coffeeMaker.brewCoffee(); // 커미 끓이기
}
}
Facade 패턴 적용 후 (1)
기존에 각 클래스들로 나누어져 있던 것들을 하나로 모은 클래스 SmartHomeFacade 생성하여 클라이언트가 wakeUp, leaveHome 메소드를 통해 위 3개의 클래스를 제어 할 수 있도록 기능을 제공한다.
public class SmartHomeFacade {
private Thermostat thermostat;
private Lights lights;
private CoffeeMaker coffeeMaker;
public SmartHomeFacade(Thermostat thermostat, Lights lights, CoffeeMaker coffeeMaker) {
this.thermostat = thermostat;
this.lights = lights;
this.coffeeMaker = coffeeMaker;
}
public void wakeUp() {
System.out.println("Waking up...");
thermostat.setTemperature(22);
lights.on();
coffeeMaker.brewCoffee();
}
public void leaveHome() {
System.out.println("Leaving home...");
thermostat.setTemperature(18);
lights.off();
}
}
클라이언트는 아래 코드에서 보듯이 Thermostat Lights CoffeeMaker 클래스들의 각 역할을 알지 못하여도, 일관된 인터페이스로 제공된 SmartHomeFacade 객체의 wakeUp, leaveHome 메소드들을 통해 쉽고 간단하게 제어 할 수 있다. 이게 Facade 패턴의 장점이다.
// Client code
public class Main {
public static void main(String[] args) {
Thermostat thermostat = new Thermostat();
Lights lights = new Lights();
CoffeeMaker coffeeMaker = new CoffeeMaker();
SmartHomeFacade smartHome = new SmartHomeFacade(thermostat, lights, coffeeMaker);
smartHome.wakeUp();
smartHome.leaveHome();
}
}
Facade 패턴 적용 전 (2)
다른 예제로도 패턴 적용을 해보도록 하겠다. 다음과 같이 각 클래스가 FileReader, FileWriter, FileDeleter 3개로 나누어져 있다고 상황을 가정해 보자.
class FileReader {
public String readFile(String filePath)
throws IOException {
return new String(Files.readAllBytes(Paths.get(filePath)));
}
}
class FileWriter {
public void writeFile(String filePath, String content)
throws IOException {
Files.write(Paths.get(filePath), content.getBytes());
}
}
class FileDeleter {
public void deleteFile(String filePath)
throws IOException {
Files.delete(Paths.get(filePath));
}
}
이걸 만약에 클라이언트에서 각각의 클래스를 생성해서 사용하려면? 다음 코드와 같이 객체를 따로 생성해서 메소드를 각각의 클래스에 맞게 상황에 따로 호출해줘야 한다.
// Client code
public class Main {
public static void main(String[] args) throws IOException {
FileReader fileReader = new FileReader();
FileWriter fileWriter = new FileWriter();
FileDeleter fileDeleter = new FileDeleter();
String readFile = fileReader.readFile("test.txt");
fileWriter.writeFile("test.txt", "쓰고싶은 문자열");
fileDeleter.deleteFile("test.txt");
}
}
Facade 패턴 적용 후 (2)
Facade 패턴을 적용하면 내부 시스템의 클래스 객체를 생성하는 것 까지도 다음과 같이 처리 할 수 있다. 해당 객체를 생성하는 시점에서 필요한 서브시스템의 클래스들을 생성자에서 생성해버린다.
class FileSystemFacade {
private FileReader fileReader;
private FileWriter fileWriter;
private FileDeleter fileDeleter;
public FileSystemFacade() {
this.fileReader = new FileReader();
this.fileWriter = new FileWriter();
this.fileDeleter = new FileDeleter();
}
public String readFile(String filePath) {
try {
return fileReader.readFile(filePath);
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
return null;
}
}
public boolean writeFile(String filePath, String content) {
try {
fileWriter.writeFile(filePath, content);
return true;
} catch (IOException e) {
System.err.println("Error writing file: " + e.getMessage());
return false;
}
}
public boolean deleteFile(String filePath) {
try {
fileDeleter.deleteFile(filePath);
return true;
} catch (IOException e) {
System.err.println("Error deleting file: " + e.getMessage());
return false;
}
}
}
클라이언트는 아래 코드에서 보듯이 FileSystemFacade 클래스의 객체만 생성하며 서브시스템의 복잡도는 줄이고 높은 편의성을 제공하는 것을 알 수 있다.
// Client code
public class Main {
public static void main(String[] args) {
FileSystemFacade fs = new FileSystemFacade();
// Write to file
boolean writeSuccess = fs.writeFile("test.txt", "Hello, Facade Pattern!");
System.out.println("File write success: " + writeSuccess);
// Read from file
String content = fs.readFile("test.txt");
System.out.println("File content: " + content);
// Delete file
boolean deleteSuccess = fs.deleteFile("test.txt");
System.out.println("File delete success: " + deleteSuccess);
}
}
Facade 패턴 요약
Facade 패턴은 복잡한 시스템을 단순화하고, 내부 시스템에 대한 의존성을 줄이며, 클라이언트가 쉽게 사용할 수 있도록 편의성을 제공 해주는 구조적 디자인 패턴이다.
-
-
-
[baekjoon] 제로 (백준 10773 java 풀이)
intro : Stack(LIFO)의 push 메소드는 맨앞에 값이 추가되며, pop 메소드를 통해 마지막에 들어온 값이 출력된다, Queue(FIFO)의 offer 메소드는 맨 뒤에 값이 추가되며, pool 메소드를 통해 첫번째로 들어온 값이 출력된다.
백준 문제링크
문제
나코더 기장 재민이는 동아리 회식을 준비하기 위해서 장부를 관리하는 중이다. 재현이는 재민이를 도와서 돈을 관리하는 중인데, 애석하게도 항상 정신없는 재현이는 돈을 실수로 잘못 부르는 사고를 치기 일쑤였다. 재현이는 잘못된 수를 부를 때마다 0을 외쳐서, 가장 최근에 재민이가 쓴 수를 지우게 시킨다.재민이는 이렇게 모든 수를 받아 적은 후 그 수의 합을 알고 싶어 한다. 재민이를 도와주자!
입력
첫 번째 줄에 정수 K가 주어진다. (1 ≤ K ≤ 100,000) 이후 K개의 줄에 정수가 1개씩 주어진다. 정수는 0에서 1,000,000 사이의 값을 가지며, 정수가 “0” 일 경우에는 가장 최근에 쓴 수를 지우고, 아닐 경우 해당 수를 쓴다. 정수가 “0”일 경우에 지울 수 있는 수가 있음을 보장할 수 있다.
출력
재민이가 최종적으로 적어 낸 수의 합을 출력한다. 최종적으로 적어낸 수의 합은 2^31 - 1보다 작거나 같은 정수이다.
문제 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayDeque;
import java.util.Deque;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int forCount = Integer.parseInt(br.readLine());
Deque<Integer> deque = new ArrayDeque<>();
for (int i = 0; i < forCount; i++) {
int input = Integer.parseInt(br.readLine());
if (input == 0) {
deque.removeLast();
} else {
deque.addLast(input);
}
}
int sum = 0;
for (Integer value : deque) {
sum += value;
}
System.out.println(sum);
}
}
-
[java] 변수
intro : java 변수의 대한 개념을 알아보자.
변수 시작
변수란 무엇일까 ?
변수는 이름 그대로 변할 수 있는 값을 뜻한다. 숫자 정수를 보관할 수 있는 이름이 a라는 데이터 저장소를 만든다. 이것을 변수라고 한다. 이렇게 변수를 만드는 것을 변수 선언이라고 한다. 이제 변수 a에는 숫자 정수를 보관 할 수 있다. 숫자 정수 뿐만 아니라 문자, 소수와 같이 다양한 종류 값을 저장할 수 있는 변수들이 있다.
public class Main {
public static void main(String[] args) {
int a; //변수 선언
a = 10; //변수 a에 10 저장 및 초기화
System.out.println(a);
System.out.println(a);
System.out.println(a);
}
}
자바에서 = 은 오른쪽에 있는 값을 왼쪽으로 저장한다는 뜻이다. 수학에서 이야기하는 두 값이 같다 (equal) 와는 다른뜻이다. 숫자를 보관할 수 있는 데이터 저장소인 변수 a에 값 10을 저장한다. 이처럼 선언한 변수에 처음으로 값을 대입해서 저장하는 것을 변수 초기화 라고 한다.
변수 값 변경
변수는 이름 그대로 변할 수 있는 수이다. 쉽게 이야기해서 변수 a에 저장된 값을 언제든지 바꿀 수 있다는 뜼이다. 이번에는 중간에 변수의 값을 변경해보자.
public class Main {
public static void main(String[] args) {
int a; //변수 선언
a = 10; //변수 초기화: a(10)
System.out.println(a); //10 출력
a = 50; //변수 값 변경: a(10 -> 50)
System.out.println(a); //50 출력
}
}
변수 선언과 초기화
변수 선언
변수를 선언하면 컴퓨터의 메모리 공간을 확보해서 그곳에 데이터를 저장할 수 있다. 그리고 변수의 이름을 통해서 해당 메모리 공간에 접근 할 수 잇다. 정리하자면, 데이터를 보관할 수 있는 공간을 만들고, 그곳에 이름을 부여한다 라고 정리 할 수 있다. 변수는 다음과 같이 하나씩 선언할 수 있고, 한번에 여러 변수를 선언 할 수도 있다.
public class Main {
public static void main(String[] args) {
int a;
int b;
int c, d;
}
}
변수 초기화
변수를 선언하고, 선언한 변수에 처음으로 값을 저장하는 것을 변수 초기화 라고 한다. 변수를 초기화 하지 않고 사용하면 오류가 발생한다. 지역변수는 꼭 개발자가 직접 초기화를 해주어야한다. (나중에 배울 클래스 변수와 인스턴스 변수는 자바가 자동으로 초기화를 진행해준다)
public class Main {
public static void main(String[] args) {
//1. 변수 선언, 초기화 각각 따로, 지역변수이기 때문에 반드시 초기화 해주어야 함
int a;
a = 1;
System.out.println(a);
int b = 2; //2. 변수 선언과 초기화를 한번에
System.out.println(b);
int c = 3, d = 4; //3. 여러 변수 선언과 초기화를 한번에
System.out.println(c);
System.out.println(d);
}
}
변수 타입1
변수는 데이터를 다루는 종류에 따라 다양한 형식이 존재한다. 이러한 형식으로는 타입이라고 하고, 우리말로는 형식 또는 형 이라고 한다. 예를들어서 int 타입, int 형, int 형식 등으로 부른다. int는 정수를 다루며, double은 실수를 다룬다. boolean 불리언 타입은 true false 값만 사용할 수 있으며 거짓 참만 판단하는 곳에서 사용한다. char는 문자 하나를 다룰때 사용하며 작은 따옴표를 사용해서 감싸야한다. String은 문자열을 다루며 큰 따옴표를 사용하여 감싸야한다. (String은 첫글자가 대문자로 시작하는 특별한 타입이다.)
public class Main {
public static void main(String[] args) {
int a = 100; //정수
double b = 10.5; //실수
boolean c = true; //불리언(boolean) true, false 입력 가능
char d = 'A'; //문자 하나
String e = "Hello Java"; //문자열, 문자열을 다루기 위한 특별한 타입
System.out.println(a);
System.out.println(b);
System.out.println(c);
System.out.println(d);
System.out.println(e);
}
}
리터럴
코드에서 개발자가 직접적은 100, 10.5, true, 'A', "Hello Java"와 같은 고정된 값을 프로그래밍 용어 리터럴 이라고 한다. 변수의 값은 변할 수 있지만 리터럴은 개발자가 직접 입력한 고정된 값이다. 따라서 리터럴 자체는 변하지 않는다.
public class Main {
public static void main(String[] args) {
int a = 100; //정수 리터럴
double b = 10.5; //실수 리터럴
boolean c = true; //불리언 리터럴
char d = 'A'; //문자 하나 리터럴
String e = "Hello Java"; //문자열 리터럴
}
}
변수 타입2
메모리를 적게 사용하면 적은 숫자를 표현 할 수 있고, 메모리를 많이 사용하면 큰 숫자를 표현할 수 있다. 변수를 선언하면 표현범위에 따라 메모리 공간을 차지한다. 그래서 필요에 맞도록 다양한 타입을 제공한다.
정수형
byte : -128 ~ 127 (1byte, 2⁸)
short : -32,768 ~ 32,767 (2byte, 2¹⁶)
int : -2,147,483,648 ~ 2,147,483,647 (약 20억) (4byte, 2³²)
long : -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 (8byte, 2⁶⁴)
실수형
float : 대략 -3.4E38 ~ 3.4E38, 7자리 정밀도 (4byte, 2³²)
double : 대략 -1.7E308 ~ 1.7E308, 15자리 정밀도 (8byte, 2⁶⁴)
기타
boolean : true , false (1byte)
char : 문자 하나(2byte)
리터럴 타입 지정
정수 리터럴은 int을 기본으로 사용한다. 따라서 int 범위까지 표현 할 수 있다. 숫자가 int 범위인 약 20억을 넘어가면 L을 붙여서 정수 리터럴을 long으로 변경해야 한다. 실수 리터럴은 기본이 double 형을 사용한다. float형을 사용하려면 f을 붙여서 float 형으로 지정해야 한다.
자주 사용하는 변수 타입
정수 - int , long : 자바는 정수에 기본으로 int 를 사용한다. 만약 20억이 넘을 것 같으면 long 을 쓰면 된다. 파일을 다룰 때는 byte 를 사용한다.
실수 - double : 실수는 고민하지 말고 double 을 쓰면 된다.
불린형 - boolean : true , false 참 거짓을 표현한다. 이후 조건문에서 자주 사용된다.
문자열 - String : 문자를 다룰 때는 문자 하나든 문자열이든 모두 String 을 사용하는 것이 편리하다.
변수 범위를 구하는 공식
2^(n-1) ~ 2^(n-1) -1
char 타입은 예외로 0 ~ 2^n -1
변수 명명 규칙
자바에서 변수의 이름을 짓는데는 규칙과 관례가 있다. 규칙은 필수이다. 규칙을 지키지 않으면 컴파일 오류가 발생한다. 관례는 필수가 아니지만 전세계 개발자가 해당 관례를 따르기 때문에 사실상 규칙이라고 생각해도 된다.
규칙
변수 이름은 숫자로 시작할 수 없다. 그러나 숫자를 이름에 포함하는 것은 가능하다. 이름에는 공백이 들어갈 수 없다. 자바의 예약어를 변수 이름으로 사용할 수 없다. 변수 이름에는 영문자(a-z , A-Z), 숫자(0-9), 달러 기호($) 또는 밑줄(_)만 사용할 수 있다.
관례
소문자로 시작하는 낙타 표기법. 변수이름은 소문자로 시작하는 것이 일반적이다. 여러 단어로 이루어진 변수 이름의 경우, 첫 번째 단어는 소문자로 시작하고 그 이후의 각 단어는 대문자로 시작하는 낙타 표기법을 사용한다.
변수 명명 규칙 정리
클래스는 대문자로 시작, 나머지는 소문자로 시작
자바에서 클래스는 이름의 첫 글자는 대문자로 시작한다. 그리고 나머지는 모두 첫 글자를 소문자로 시작한다. 여기에 예외가 딱 2가지 있는데, 상수는 모두 대문자를 사용하고 언더바로 구분한다. 패키지는 모두 소문자를 사용한다.
-
-
[baekjoon] 베르트랑 공준 (백준 4948 java 풀이)
intro : 에라토스테네스의 기법을 통한 소수 찾기방법을 알아보자.
백준 문제링크
문제
베르트랑 공준은 임의의 자연수 n에 대하여, n보다 크고, 2n보다 작거나 같은 소수는 적어도 하나 존재한다는 내용을 담고 있다. 이 명제는 조제프 베르트랑이 1845년에 추측했고, 파프누티 체비쇼프가 1850년에 증명했다. 예를 들어, 10보다 크고, 20보다 작거나 같은 소수는 4개가 있다. (11, 13, 17, 19) 또, 14보다 크고, 28보다 작거나 같은 소수는 3개가 있다. (17,19, 23) 자연수 n이 주어졌을 때, n보다 크고, 2n보다 작거나 같은 소수의 개수를 구하는 프로그램을 작성하시오.
입력
입력은 여러 개의 테스트 케이스로 이루어져 있다. 각 케이스는 n을 포함하는 한 줄로 이루어져 있다 입력의 마지막에는 0이 주어진다.
출력
각 테스트 케이스에 대해서, n보다 크고, 2n보다 작거나 같은 소수의 개수를 출력한다.
제한
1 ≤ n ≤ 123,456
문제 풀이1 (제곱근을 활용한 문제풀이 844ms)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringBuilder sb = new StringBuilder();
int input;
while ((input = Integer.parseInt(br.readLine())) != 0) {
int count = 0;
for (int i = input + 1; i <= input * 2; i++) {
if (isPrime(i)) {
count++;
}
}
sb.append(count).append("\n");
}
System.out.println(sb);
}
public static boolean isPrime(int input) {
if (input <= 1) return false;
if (input <= 3) return true;
if (input % 2 == 0 || input % 3 == 0) return false;
for (int i = 2; i <= Math.sqrt(input); i++) {
if (input % i == 0) {
return false;
}
}
return true;
}
}
문제 풀이2 (에라토스테네스의 체 기법 사용 164ms)
import java.io.*;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
StringBuilder sb = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
boolean[] isPrime = new boolean[123456 * 2 + 1];
Arrays.fill(isPrime, true);
isPrime[0] = false;
isPrime[1] = false;
for (int i = 2; i * i <= isPrime.length; i++) {
if (isPrime[i]) {
for (int j = i * i; j < isPrime.length; j += i) {
isPrime[j] = false;
}
}
}
int input;
while ((input = Integer.parseInt(br.readLine())) != 0) {
int count = 0;
for (int i = input + 1; i <= input * 2; i++) {
if (isPrime[i]) {
count++;
}
}
sb.append(count).append("\n");
}
bw.write(sb.toString());
bw.flush();
bw.close();
br.close();
}
}
-
-
-
-
[baekjoon] 최소공배수 (백준 13241 java 풀이)
intro : 입력값이 10의8승까지도 나온다는 점을 눈여겨 봐야한다.
백준 문제링크
문제
정수 B에 0보다 큰 정수인 N을 곱해 정수 A를 만들 수 있다면, A는 B의 배수이다. 10은 5의 배수이다
(52 = 10) 10은 10의 배수이다 (101 = 10) 6은 1의 배수이다(1*6 = 6) 20은 1,2,4,5,10,20의 배수이다. 2와 5의 최소공배수는 10이고, 그 이유는 2와 5보다 작은 공배수가 없기 때문이다. 10과 20의 최소공배수는 20이다. 5와 3의 최소공배수는 15이다. 당신은 두 수에 대하여 최소공배수를 구하는 프로그램을 작성 하는 것이 목표이다.
입력
한 줄에 두 정수 A와 B가 공백으로 분리되어 주어진다. 50%의 입력 중 A와 B는 1000(103)보다 작다. 다른 50%의 입력은 1000보다 크고 100000000(108)보다 작다.
추가: 큰 수 입력에 대하여 변수를 64비트 정수로 선언하시오. C/C++에서는 long long int를 사용하고, Java에서는 long을 사용하시오.
출력
A와 B의 최소공배수를 한 줄에 출력한다.
문제 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
long first = Long.parseLong(st.nextToken());
long second = Long.parseLong(st.nextToken());
long lcm = first * second / gcd(Math.max(first, second), Math.min(first, second));
System.out.println(lcm);
}
public static long gcd(long a, long b) {
while (b != 0) {
long r = a % b;
a = b;
b = r;
}
return a;
}
}
-
[baekjoon] 최소공배수 (백준 1934 java 풀이)
intro : [최소 공배수 = a * b / 최대 공약수] 라는 공식으로 풀이한다.
백준 문제링크
문제
두 자연수 A와 B에 대해서, A의 배수이면서 B의 배수인 자연수를 A와 B의 공배수라고 한다. 이런 공배수 중에서 가장 작은 수를 최소공배수라고 한다. 예를 들어, 6과 15의 공배수는 30, 60, 90등이 있으며, 최소 공배수는 30이다. 두 자연수 A와 B가 주어졌을 때, A와 B의 최소공배수를 구하는 프로그램을 작성하시오.
입력
첫째 줄에 테스트 케이스의 개수 T(1 ≤ T ≤ 1,000)가 주어진다. 둘째 줄부터 T개의 줄에 걸쳐서 A와 B가 주어진다. (1 ≤ A, B ≤ 45,000)
출력
첫째 줄부터 T개의 줄에 A와 B의 최소공배수를 입력받은 순서대로 한 줄에 하나씩 출력한다.
문제 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int forCount = Integer.parseInt(br.readLine());
for (int i = 0; i < forCount; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
int first = Integer.parseInt(st.nextToken());
int second = Integer.parseInt(st.nextToken());
int lcm = first * second / gcd(Math.max(first, second), Math.min(first, second));
System.out.println(lcm);
}
}
public static int gcd(int a, int b) {
while (b != 0) {
int r = a % b;
a = b;
b = r;
}
return a;
}
}
문제 해석
LCM(Least Common Multiple 최소 공배수) 를 구하기 위해서는 주어진 자연수 A,B의 GCD(Greatest Common Divisor 최대 공약수)의 값을 구해서 A * B / GCD 의 값을 도출하는게 포인트이다. 여기서 GCD는 유클리드 호제법으로 풀이하는데, A > B 라는 조건하에 A를 B로 나누어 나머지가 0이 될때까지 반복하여 B값을 구하는 이론이다. 좀 더 설명하면 다음과 같은 순서를 거친다.
A = 18, B = 12
18 % 12 = 나머지 6
12 % 6 = 나머지 0
최대 공약수 : 6
-
[baekjoon] 서로 다른 부분 문자열의 개수 (백준 11478 java 풀이)
intro : 부분 문자열을 구하는 로직이 포인트
백준 문제링크
문제
문자열 S가 주어졌을 때, S의 서로 다른 부분 문자열의 개수를 구하는 프로그램을 작성하시오. 부분 문자열은 S에서 연속된 일부분을 말하며, 길이가 1보다 크거나 같아야 한다. 예를 들어, ababc의 부분 문자열은 a, b, a, b, c, ab, ba, ab, bc, aba, bab, abc, abab, babc, ababc가 있고, 서로 다른것의 개수는 12개이다.
입력
첫째 줄에 문자열 S가 주어진다. S는 알파벳 소문자로만 이루어져 있고, 길이는 1,000 이하이다.
출력
첫째 줄에 S의 서로 다른 부분 문자열의 개수를 출력한다.
문제 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
Set<String> uniqueStr = new HashSet<>();
String input = br.readLine();
for (int i = 0; i < input.length(); i++) {
for (int j = i + 1; j <= input.length(); j++) {
String subStr = input.substring(i, j);
uniqueStr.add(subStr);
}
}
System.out.println(uniqueStr.size());
}
}
-
[baekjoon] 대칭 차집합 (백준 1269 java 풀이)
intro : retainAll 메소도는 교집합을 구하는데 적합하며, removeAll 메소드는 차집합을 구하는데 적합하다.
백준 문제링크
문제
자연수를 원소로 갖는 공집합이 아닌 두 집합 A와 B가 있다. 이때, 두 집합의 대칭 차집합의 원소의 개수를 출력하는 프로그램을 작성하시오. 두 집합 A와 B가 있을 때, (A-B)와 (B-A)의 합집합을 A와 B의 대칭 차집합이라고 한다. 예를 들어, A = { 1, 2, 4 } 이고, B = { 2, 3, 4, 5, 6 } 라고 할 때, A-B = { 1 } 이고, B-A = { 3, 5, 6 } 이므로, 대칭 차집합의 원소의 개수는 1 + 3 = 4개이다.
입력
첫째 줄에 집합 A의 원소의 개수와 집합 B의 원소의 개수가 빈 칸을 사이에 두고 주어진다. 둘째 줄에는 집합 A의 모든 원소가, 셋째 줄에는 집합 B의 모든 원소가 빈 칸을 사이에 두고 각각 주어진다. 각 집합의 원소의 개수는 200,000을 넘지 않으며, 모든 원소의 값은 100,000,000을 넘지 않는다.
출력
첫째 줄에 대칭 차집합의 원소의 개수를 출력한다.
문제 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int firstValue = Integer.parseInt(st.nextToken());
int secondValue = Integer.parseInt(st.nextToken());
Set<String> firstSet = new HashSet<>();
st = new StringTokenizer(br.readLine());
for (int i = 0; i < firstValue; i++) {
String value = st.nextToken();
firstSet.add(value);
}
Set<String> secondSet = new HashSet<>();
st = new StringTokenizer(br.readLine());
for (int i = 0; i < secondValue; i++) {
secondSet.add(st.nextToken());
}
Set<String> retainSet = new HashSet<>(firstSet);
retainSet.retainAll(secondSet);
System.out.println((firstSet.size() - retainSet.size()) + (secondSet.size() - retainSet.size()));
}
}
-
-
[baekjoon] 슷자 카드 2 (백준 10816 java 풀이)
intro : map을 통해 문제를 푸는게 포인트인 문제.
백준 문제링크
문제
숫자 카드는 정수 하나가 적혀져 있는 카드이다. 상근이는 숫자 카드 N개를 가지고 있다. 정수 M개가 주어졌을 때, 이 수가 적혀있는 숫자 카드를 상근이가 몇 개 가지고 있는지 구하는 프로그램을 작성하시오.
입력
첫째 줄에 상근이가 가지고 있는 숫자 카드의 개수 N(1 ≤ N ≤ 500,000)이 주어진다. 둘째 줄에는 숫자 카드에 적혀있는 정수가 주어진다. 숫자 카드에 적혀있는 수는 -10,000,000보다 크거나 같고, 10,000,000보다 작거나 같다. 셋째 줄에는 M(1 ≤ M ≤ 500,000)이 주어진다. 넷째 줄에는 상근이가 몇 개 가지고 있는 숫자 카드인지 구해야 할 M개의 정수가 주어지며, 이 수는 공백으로 구분되어져 있다. 이 수도 -10,000,000보다 크거나 같고, 10,000,000보다 작거나 같다.
출력
첫째 줄에 입력으로 주어진 M개의 수에 대해서, 각 수가 적힌 숫자 카드를 상근이가 몇 개 가지고 있는지를 공백으로 구분해 출력한다.
문제 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int inputCount = Integer.parseInt(br.readLine());
Map<Integer, Integer> inputMap = new HashMap<>();
StringTokenizer st = new StringTokenizer(br.readLine());
for (int i = 0; i < inputCount; i++) {
int input = Integer.parseInt(st.nextToken());
inputMap.put(input, inputMap.getOrDefault(input, 0) + 1);
}
StringBuilder sb = new StringBuilder();
int findCount = Integer.parseInt(br.readLine());
st = new StringTokenizer(br.readLine());
for (int i = 0; i < findCount; i++) {
int findValue = Integer.parseInt(st.nextToken());
sb.append(inputMap.getOrDefault(findValue, 0)).append(" ");
}
System.out.println(sb);
}
}
문제 해석
문제 풀이의 포인트는 map에 존재하는 값인지 확인후 존재한다면 기존값의 value 값에 + 1를 한다는점, 만약에 없는 값이면 0으로 셋팅하여 보관하고 차후 검색하는 키값을 통해 map의 value 값을 StringBuilder를 통해 출력하는 문제 (기존에 풀었던 숫자 카드 문제와 큰 틀은 유사하다)
-
[baekjoon] 나는야 포켓몬 마스터 이다솜 (백준 1620 java 풀이)
intro : 문제가 너무길고 몰입이 안되어서 화가났다.
백준 문제링크
문제
안녕? 내 이름은 이다솜. 나의 꿈은 포켓몬 마스터야. 일단 포켓몬 마스터가 되기 위해선 포켓몬을 한 마리 잡아야겠지? 근처 숲으로 가야겠어. (뚜벅 뚜벅) 얏! 꼬렛이다. 꼬렛? 귀여운데, 나의 첫 포켓몬으로 딱 어울린데? 내가 잡고 말겠어. 가라! 몬스터볼~ (펑!) 헐랭… 왜 안 잡히지?ㅜㅜ 몬스터 볼만 던지면 되는 게 아닌가…ㅜㅠ (터벅터벅) 어? 누구지?
오박사 : 나는 태초마을의 포켓몬 박사 오민식 박사라네. 다솜아, 포켓몬을 잡을 때는, 일단 상대 포켓몬의 체력을 적당히 바닥으로 만들어놓고 몬스터 볼을 던져야 한단다. 자, 내 포켓몬 이상해꽃으로 한번 잡아보렴. 포켓몬의 기술을 쓰는 것을 보고 포켓몬을 줄지 안줄지 결정을 하겠네. 자 한번 해보아라. 다솜아.
이다솜 : 이상해꽃이라…음.. 꽃이니깐 왠지 햇빛을 받아서 공격을 할 것 같은데… 음… 이상해꽃! 햇빛공격!!!
(꼬렛이 이상해꽃에게 공격을 받아 체력이 25 감소했다.) 가라! 몬스터 볼!!! (꼬렛을 잡았습니다.) 야호! 신난다. 꼬렛을 잡았다.
오박사 : 오우!! 방금 쓴 공격은 솔라빔이라고 하네.. 어떻게 공격을 한 건가? 솔라빔이란 공격에 대해서 공부를 한 건가?
이다솜 : 꽃이니깐 왠지 햇빛을 제대로 받으면 광합성을 해서 음.. 그냥 그럴 것 같아서요 ☞☜
오박사 : 다른 아이들은 넝쿨채찍이나, 나뭇잎 공격을 하는데, 다솜이는 역시 뭔가 다르구나. 그럼 나와 함께 연구소로 가자꾸나. 내가 포켓몬을 한 마리 줄 테니, 너의 꿈을 펼쳐보아라. 꿈은 이루어진단다.
이다솜 : 네! 오박사님, 고마워요.ㅜㅜ
오박사 : 가자. 나의 연구소는 너의 옆집의 아랫집이란다. 같이 가도록하자. 지금 포켓몬을 주마.
이다솜 : 네. 야호!!
오영식 : 어? 오박사님 얘는 누구인가요?
오박사 : 얘는 너의 라이벌이 될 친구 이다솜이라고 하네. 자, 포켓몬을 한 마리 골라보도록 해봐라 다솜아. 레이디퍼스트 네가 먼저 골라봐라.
이다솜 : 저는 생각해둔 포켓몬이 있어요. 피카츄 골라도 될까요?
오박사 : 그래 여기 피카츄가 한 마리 있단다. 피카츄를 가져가거라.
오영식 : 그럼 저는 이브이를 가져가겠어요. 그럼 나중에 보자 이다솜.
이다솜 : 그럼 꼬렛을 다시 잡으러 가야겠다. 영식아, 그리고 민식박사님 빠잉!
이다솜 : 피카츄 공격!
가라 몬스터 볼!
이다솜 : 야호! 신난다. 꼬렛을 잡았다!!!!!
이다솜 : 그럼! 일단 사천왕을 이기고 오겠어!
이다솜 : 여기가 사천왕과 대결하려면 가야하는 곳인가..
경비원 : 사천왕과 대결을 하려면, 마을의 체육관 리더를 이겨서 배지를 8개를 모아야 한다네… 배지를 모아서 오도록 하게
이다솜 : 잉ㅠㅜ… 그럼 배지부터 모아야 하는구나ㅠㅜㅠㅜ 나쁘당 그냥 좀 봐주지..
<1 년 후>
그동안의 줄거리 : 이다솜은 일단 상록 숲의 체육관 리더에게 도전을 했다. 하지만 상록숲 체육관의 리더는 실종된 상태. 따라서 회색마을부터 도전하기로 했다. 체육관의 리더를 이기면서, 로켓단을 해체시키기도 하고, 여러 가지 사건도 있었다. 결국 전설의 포켓몬도 잡고, 이제 사천왕을 이기려고 도전하기로 했다. 사천왕은 모두 가볍게 이기고, 이제 마지막 라이벌 오!영!식! 이다.
오영식 : 훗. 1년 전의 그 이다솜이 사천왕을 이기고 현재 포켓몬 마스터인 나에게 덤벼? 어디 한번 덤벼보시지.
이다솜 : 헐랭… 나를 우습게보네…. 한번 두고 보시지! 그럼 대결이닷!
이다솜 : 휴… 이겼다.
오영식 : 내가 지다니 분하다. ㅜㅜ
오박사 : 그럼 다솜아 이제 진정한 포켓몬 마스터가 되기 위해 도감을 완성시키도록 하여라. 일단 네가 현재 가지고 있는 포켓몬 도감에서 포켓몬의 이름을 보면 포켓몬의 번호를 말하거나, 포켓몬의 번호를 보면 포켓몬의 이름을 말하는 연습을 하도록 하여라. 나의 시험을 통과하면, 내가 새로 만든 도감을 주도록 하겠네.
입력
첫째 줄에는 도감에 수록되어 있는 포켓몬의 개수 N이랑 내가 맞춰야 하는 문제의 개수 M이 주어져. N과 M은 1보다 크거나 같고, 100,000보다 작거나 같은 자연수인데, 자연수가 뭔지는 알지? 모르면 물어봐도 괜찮아. 나는 언제든지 질문에 답해줄 준비가 되어있어.
둘째 줄부터 N개의 줄에 포켓몬의 번호가 1번인 포켓몬부터 N번에 해당하는 포켓몬까지 한 줄에 하나씩 입력으로 들어와. 포켓몬의 이름은 모두 영어로만 이루어져있고, 또, 음… 첫 글자만 대문자이고, 나머지 문자는 소문자로만 이루어져 있어. 아참! 일부 포켓몬은 마지막 문자만 대문자일 수도 있어. 포켓몬 이름의 최대 길이는 20, 최소 길이는 2야. 그 다음 줄부터 총 M개의 줄에 내가 맞춰야하는 문제가 입력으로 들어와. 문제가 알파벳으로만 들어오면 포켓몬 번호를 말해야 하고, 숫자로만 들어오면, 포켓몬 번호에 해당하는 문자를 출력해야해. 입력으로 들어오는 숫자는 반드시 1보다 크거나 같고, N보다 작거나 같고, 입력으로 들어오는 문자는 반드시 도감에 있는 포켓몬의 이름만 주어져. 그럼 화이팅!!!
출력
첫째 줄부터 차례대로 M개의 줄에 각각의 문제에 대한 답을 말해줬으면 좋겠어!!!. 입력으로 숫자가 들어왔다면 그 숫자에 해당하는 포켓몬의 이름을, 문자가 들어왔으면 그 포켓몬의 이름에 해당하는 번호를 출력하면 돼. 그럼 땡큐~ 이게 오박사님이 나에게 새로 주시려고 하는 도감이야. 너무 가지고 싶다ㅠㅜ. 꼭 만점을 받아줬으면 좋겠어!! 파이팅!!!
문제 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int inputCount = Integer.parseInt(st.nextToken());
int findCount = Integer.parseInt(st.nextToken());
Map<String, Integer> inputStringMap = new HashMap<>();
Map<Integer, String> inputIntMap = new HashMap<>();
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= inputCount + findCount; i++) {
if (i <= inputCount) {
String input = br.readLine();
inputStringMap.put(input, i);
inputIntMap.put(i, input);
} else {
String findValue = br.readLine();
try {
int index = Integer.parseInt(findValue);
String name = inputIntMap.get(index);
sb.append(name).append("\n");
} catch (NumberFormatException e) {
Integer index = inputStringMap.get(findValue);
sb.append(index).append("\n");
}
}
}
System.out.println(sb);
}
}
문제 해석
문제의 내용을 정리하면 사실 간단하다. 입력값으로 들어오는 첫줄은 공백으로 구분되어있는데 공백을 기준으로 첫번째 값은 입력되는 입력받아야 할 반복문 총 횟수, 두번쨰 값은 질문 할 반복 횟수 라고 볼 수 있다. 그래서 해당 값만큼 반복문으로 입력을 받는데, 시간복잡도를 고려하여 map으로 변수를 구성한다. 그 이유는 문자열로 값이 들어오면 정수로 출력해줘야 하고, 정수로 값이 들어오면 문자열로 값을 출력해줘여 하기 때문에 map이 가장 적합한 자료구조의 타입으로 이 문제에서는 사용 될 수 있다.
추가적으로 입력되는 값이 정수인지 문자인지 판단하는 부분은 instanceof를 통해 검사할수 없다. br.readline은 string 타입으로 반환값이 고정이기 때문에 검사할수 없으니 try catch 구조로 Integer 타입으로 변환하였을때, exceptio이 발생하는지 여부에 따라 문자열인지 정수인지 판단하여 분기 로직을 구성한다. 출력은 StringBuilder를 통해 한번에 출력하는 것이 포인트, System.out.println은 병목현상을 유발하는 주된 원인이기 때문에 항상 조심해야한다. (시간적으로 거의 두배 이상의 차이가 나기도 한다.)
-
[baekjoon] 회사에 있는 사람 (백준 7785 java 풀이)
intro : 자료구조를 많이 알면 알수록 문제풀이는 쉬워진다. 또한 시간 복잡도의 계산능력이 좋아지는거 같다.
백준 문제링크
문제
상근이는 세계적인 소프트웨어 회사 기글에서 일한다. 이 회사의 가장 큰 특징은 자유로운 출퇴근 시간이다. 따라서, 직원들은 반드시 9시부터 6시까지 회사에 있지 않아도 된다. 각 직원은 자기가 원할 때 출근할 수 있고, 아무때나 퇴근할 수 있다. 상근이는 모든 사람의 출입카드 시스템의 로그를 가지고 있다. 이 로그는 어떤 사람이 회사에 들어왔는지, 나갔는지가 기록되어져 있다. 로그가 주어졌을 때, 현재 회사에 있는 모든 사람을 구하는 프로그램을 작성하시오.
입력
첫째 줄에 로그에 기록된 출입 기록의 수 n이 주어진다. (2 ≤ n ≤ 106) 다음 n개의 줄에는 출입 기록이 순서대로 주어지며, 각 사람의 이름이 주어지고 “enter”나 “leave”가 주어진다. “enter”인 경우는 출근, “leave”인 경우는 퇴근이다. 회사에는 동명이인이 없으며, 대소문자가 다른 경우에는 다른 이름이다. 사람들의 이름은 알파벳 대소문자로 구성된 5글자 이하의 문자열이다.
출력
현재 회사에 있는 사람의 이름을 사전 순의 역순으로 한 줄에 한 명씩 출력한다.
문제 풀이1 (잘못된 풀이 - 런타임 에러 (ConcurrentModification))
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;
public class Main {
public static void main(String[] args) throws IOException {
Map<String, String> map = new HashMap<>();
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int forCount = Integer.parseInt(br.readLine());
for (int i = 0; i < forCount; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
map.put(st.nextToken(), st.nextToken());
}
for (String key : map.keySet()) {
String status = map.get(key);
if (!status.equals("enter")) {
map.remove(key);
}
}
TreeSet<String> treeSet = new TreeSet<>(map.keySet());
Iterator<String> iterator = treeSet.descendingIterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
문제 풀이2
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
Set<String> set = new HashSet<>();
int forCount = Integer.parseInt(br.readLine());
for (int i = 0; i < forCount; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
String name = st.nextToken();
String status = st.nextToken();
if (status.equals("enter")) {
set.add(name);
} else {
set.remove(name);
}
}
StringBuilder sb = new StringBuilder();
TreeSet<String> sortSet = new TreeSet<>(set);
Iterator<String> iterator = sortSet.descendingIterator();
while (iterator.hasNext()) {
sb.append(iterator.next());
sb.append("\n");
}
System.out.println(sb);
}
}
문제해석
문제를 보고 떠오른 아이디어는, 입출입 값을 map이나 set을 통해 관리하고 leave를 한 인원은 제외한다음, Collection 끼리의 타입 변환은 가능하기에 정렬 기능을 제공하는 자료구조인 TreeSet으로 변환 후, 역순으로 값을 출력하자가 생각이었다. (list로도 변환할수 있고, list의 역순 출력도 고려해 보았다.)
이때 자료구조는 map이 더 나아보여서 map으로 문제를 풀어보았는데, remove를 하는 시점에서 Concurrent Modification Exception이 발생하였으며, map에서는 순회하면서 remove를 하는게 예외를 발생시킨다는 점을 인지하게 되었다. 다만 set은 map과 내부 구조가 달라, 순회하면서 remove를 하여도 예외가 발생하지 않아 문제풀이 2번 방식으로 문제를 풀 수 있다는것이 포인트가 될 수 있다.
또한 시간적으로 단축시켜야 하기에 병목현상이 발생할수 있는 System.out.println을 반복문에서 사용하지않고, StringBuilder를 통해 문자열을 구성한 후, 한번에 출력하는 형식으로 로직을 구성하였다. StringBuilder를 사용하기전에는 1초가 걸리는 로직이, 후에 0.5초로 엄청난 속도 개선이 이루어 졌다.
-
-
[baekjoon] 숫자 카드 (백준 10815 java 풀이)
intro : Map을 사용할지, Set을 사용할지 고민해 보는게 좋다.
백준 문제링크
문제
숫자 카드는 정수 하나가 적혀져 있는 카드이다. 상근이는 숫자 카드 N개를 가지고 있다. 정수 M개가 주어졌을 때, 이 수가 적혀있는 숫자 카드를 상근이가 가지고 있는지 아닌지를 구하는 프로그램을 작성하시오.
입력
첫째 줄에 상근이가 가지고 있는 숫자 카드의 개수 N(1 ≤ N ≤ 500,000)이 주어진다. 둘째 줄에는 숫자 카드에 적혀있는 정수가 주어진다. 숫자 카드에 적혀있는 수는 -10,000,000보다 크거나 같고, 10,000 000보다 작거나 같다. 두 숫자 카드에 같은 수가 적혀있는 경우는 없다. 셋째 줄에는 M(1 ≤ M ≤ 500,000)이 주어진다. 넷째 줄에는 상근이가 가지고 있는 숫자 카드인지 아닌지를 구해야 할 M개의 정수가 주어지며, 이 수는 공백으로 구분되어져 있다. 이 수도 -10,000,000보다 크거나 같고, 10,000,000보다 작거나 같다
출력
첫째 줄에 입력으로 주어진 M개의 수에 대해서, 각 수가 적힌 숫자 카드를 상근이가 가지고 있으면 1을, 아니면 0을 공백으로 구분해 출력한다.
문제 풀이 1 (Map을 이용한 문제풀이)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
br.readLine();
String[] getCartSplit = br.readLine().split(" ");
int findCount = Integer.parseInt(br.readLine());
int[] result = new int[findCount];
String[] findCardSplit = br.readLine().split(" ");
Map<Integer, Boolean> cardMap = new HashMap<>();
Map<Integer, Boolean> findMap = new LinkedHashMap<>();
for (String getCard : getCartSplit) cardMap.put(Integer.parseInt(getCard), true);
for (String findCard : findCardSplit) findMap.put(Integer.parseInt(findCard), true);
int index = 0;
for (Integer findKey : findMap.keySet()) {
Boolean check = cardMap.getOrDefault(findKey, false);
if (check) {
result[index] = 1;
}
index++;
}
StringBuilder sb = new StringBuilder();
for (int value : result) {
sb.append(value).append(" ");
}
System.out.println(sb);
}
}
문제 풀이 2 (Set을 이용한 문제풀이)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int getCardCount = Integer.parseInt(br.readLine());
StringTokenizer st = new StringTokenizer(br.readLine());
Set<String> getCardSet = new HashSet<>();
for (int i = 0; i < getCardCount; i++) getCardSet.add(st.nextToken());
StringBuilder sb = new StringBuilder();
int findCardCount = Integer.parseInt(br.readLine());
st = new StringTokenizer(br.readLine());
for (int i = 0; i < findCardCount; i++) {
boolean check = getCardSet.contains(st.nextToken());
if (check) {
sb.append("1").append(" ");
} else {
sb.append("0").append(" ");
}
}
System.out.println(sb);
}
}
문제 해석
주어진 조건을 잘 살펴보면 N(1 ≤ N ≤ 500,000), M(1 ≤ M ≤ 500,000) 즉 N 과 M의 길이가 각각 50만씩, 이중 반복문으로 카드의 존재여부를 확인하고자 하는 로직을 구성한다면 ? 단순히 생각했을때만 해도 2500억 번의 연산횟수가 시도된다.(물론 정확하게는 아니고 대략적인 값이다.)
1억번당 1초의 연산 속도를 가정한다면 이 문제는 단순 반복문으로 풀 수 있는 문제가 아닌것을 눈치 해야한다. 그렇다면 시간 복잡도가 1에 수렴하면서 빠르게 존재여부를 확인할 수 있는 자료구조는 무엇이 있을까 ?
바로 Hash 자료구조이다. 보통 Hash 자료구조에는 크게 두가지가 있는데 Map 과 Set이 존재한다. Map.get() 메소드는 1의 시간복잡도에 수렴하며, Set.contains() 메소드 또한 1의 시간복잡도에 수렴한다 (보통 그렇다.) 이 포인트를 잘 생각하여 상근이가 가지고 있는 카드가 숫자카드에 존재하는 카드인지 체크하는 로직을 구성하는것이 문제 풀이의 옳은 방법이 된다.
초기 문제풀이1번에서는 LinkedHashMap을 통해 자료구조를 구성해서 문제풀이를 시도했는데, 문제를 풀고나니 굳이 순서를 지키면서 변수를 구성해야 될 필요도 없다는 것을 알게되었으며, 문제 조건에서 주어진 값이 중복된 값이 없다고 하였으니 Map이 아닌 Set을 통해 풀이를 구성하는게 이 문제에 더 적합하다고 생각된다.
-
[baekjoon] 세 막대 (백준 14215 java 풀이)
intro : 삼각형의 가장 큰 변의 길이를 줄여가면서 삼각형의 조건이 만족하는지 확인하고, 가장 처음으로 만족하는 변으 길이가 되었을때 둘레의 길이를 출력하는게 포인트
백준 문제링크
문제
영선이는 길이가 a, b, c인 세 막대를 가지고 있고, 각 막대의 길이를 마음대로 줄일 수 있다. 영선이는 세 막대를 이용해서 아래 조건을 만족하는 삼각형을 만들려고 한다. 각 막대의 길이는 양의 정수이다 세 막대를 이용해서 넓이가 양수인 삼각형을 만들 수 있어야 한다. 삼각형의 둘레를 최대로 해야 한다. a, b, c가 주어졌을 때, 만들 수 있는 가장 큰 둘레를 구하는 프로그램을 작성하시오.
입력
첫째 줄에 a, b, c (1 ≤ a, b, c ≤ 100)가 주어진다.
출력
첫째 줄에 만들 수 있는 가장 큰 삼각형의 둘레를 출력한다.
문제 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int value1 = Integer.parseInt(st.nextToken());
int value2 = Integer.parseInt(st.nextToken());
int value3 = Integer.parseInt(st.nextToken());
int[] arr = new int[]{value1, value2, value3};
if (isTriangleCheck(arr)) {
System.out.println(value1 + value2 + value3);
} else {
int maxIndex = findMaxIndex(arr);
for (int i = arr[maxIndex] - 1; i >= 0; i--) {
arr[maxIndex] = i;
if (isTriangleCheck(arr, maxIndex)) {
System.out.println(arr[0] + arr[1] + arr[2]);
return;
}
}
}
}
public static boolean isTriangleCheck(int[] arr, int maxIndex) {
int remainSum = 0;
for (int i = 0; i < arr.length; i++) {
if (i != maxIndex) {
remainSum += arr[i];
}
}
return arr[maxIndex] < remainSum;
}
public static boolean isTriangleCheck(int[] arr) {
int maxIndex = findMaxIndex(arr);
return isTriangleCheck(arr, maxIndex);
}
public static int findMaxIndex(int[] arr) {
int maxValue = Integer.MIN_VALUE;
int maxIndex = -1;
for (int i = 0; i < arr.length; i++) {
if (maxValue < arr[i]) {
maxValue = arr[i];
maxIndex = i;
}
}
return maxIndex;
}
}
-
[baekjoon] 삼각형과 세 변 (백준 5073 java 풀이)
intro : 삼각형의 조건을 만족하는 메소드를 구성하는데 쉽게풀려다가 오래걸렸는데, 가장원초적인 방법이 가장 최적화된 방법인 것 같다.
백준 문제링크
문제
삼각형의 세 변의 길이가 주어질 때 변의 길이에 따라 다음과 같이 정의한다.
Equilateral : 세 변의 길이가 모두 같은 경우
Isosceles : 두 변의 길이만 같은 경우
Scalene : 세 변의 길이가 모두 다른 경우
단 주어진 세 변의 길이가 삼각형의 조건을 만족하지 못하는 경우에는 “Invalid” 를 출력한다. 예를 들어 6, 3, 2가 이 경우에 해당한다. 가장 긴 변의 길이보다 나머지 두 변의 길이의 합이 길지 않으면 삼각형의 조건을 만족하지 못한다. 세 변의 길이가 주어질 때 위 정의에 따른 결과를 출력하시오.
입력
각 줄에는 1,000을 넘지 않는 양의 정수 3개가 입력된다. 마지막 줄은 0 0 0이며 이 줄은 계산하지 않는다.
출력
각 입력에 맞는 결과 (Equilateral, Isosceles, Scalene, Invalid) 를 출력하시오.
문제 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String str;
while (!(str = br.readLine()).equals("0 0 0")) {
StringTokenizer st = new StringTokenizer(str);
int value1 = Integer.parseInt(st.nextToken());
int value2 = Integer.parseInt(st.nextToken());
int value3 = Integer.parseInt(st.nextToken());
if (isTriangleCheck(value1, value2, value3)) {
if ((value1 == value2) && (value2 == value3)) {
System.out.println("Equilateral");
} else if ((value1 != value2) && (value1 != value3) && (value2 != value3)) {
System.out.println("Scalene");
} else {
System.out.println("Isosceles");
}
}
}
}
public static boolean isTriangleCheck(int value1, int value2, int value3) {
int[] arr = new int[]{value1, value2, value3};
int maxValue = Integer.MIN_VALUE;
int maxIndex = -1;
for (int i = 0; i < arr.length; i++) {
if (maxValue < arr[i]) {
maxValue = arr[i];
maxIndex = i;
}
}
int remainSum = 0;
for (int i = 0; i < arr.length; i++) {
if (i != maxIndex) {
remainSum += arr[i];
}
}
if (arr[maxIndex] < remainSum) {
return true;
} else {
System.out.println("Invalid");
return false;
}
}
}
-
-
[baekjoon] 대지 (백준 9063 java 풀이)
intro : min max를 구하는 방법은 생각보다 간단하니 꼭 숙지하자
백준 문제링크
문제
임씨는 1950 년 한국전쟁으로 많은 손해를 본 사람들 중 하나다. 전쟁 통에 손해보지 않은 사람이 어디 있을까 만은 그는 6.25 가 일어나기 전만 해도 충청도 지방에 넓은 대지를 소유한 큰 부자였다. 전쟁이 나자 임씨는 땅문서와 값 나가는 것들만 챙겨서 일본으로 피난을 가지만 피난 중에 그만 땅문서를 잃어버리고 만다. 전쟁이 끝난 후에 임씨의 땅은 이미 다른 사람들의 논밭이 되어 있었고, 임씨는 땅을 되찾으려 했지만 문서가 없으니 생떼 쓰는 것과 다를 바 없었다. 이러다가 임씨는 길바닥에 나앉게 생겼다.
이때, 임씨에게 좋은 생각이 떠올랐으니 바로 자신이 습관처럼 땅 깊숙이 뭔가 표식을 해놓았던 사실이다. 임씨는 한적할 때마다 자신의 논밭을 거닐다가 땅속 깊은 곳에 자신의 이름이 씌어진 옥구슬을 묻어놓았던 것이다. 즉, 어떤 지점에서 그의 이름이 적힌 옥구슬이 나온다면 그 지점은 예전에 임씨의 땅이었다는 것을 증명하는 것이다.
임씨는 즉시 민사소송을 통해 자신의 땅을 찾고자 했고 논리적인 근거를 들어 옥구슬이 나오는 지점이 원래 자신의 땅의 한 지점이었다는 것을 주장하여 결국 담당판사를 설득하는 데에 성공하였다. 담당판사는 다음과 같은 판결을 내렸다. “ 6.25 이전의 개인소유 대지들은 99%가 남북, 동서 방향으로 평행한 직사각형 모양이었으므로, 임씨의 이름이 새겨진 옥구슬이 나오는 모든 지점을 포함하는 가장 작은 남북, 동서 방향으로 평행한 변을 갖는 직사각형의 대지를 임씨의 소유로 인정한다.” 임씨는 많은 손해를 보는 셈이지만 더 이상을 요구할 만한 근거가 없었기 때문에 이 판결을 따르기로 했다.
임씨의 이름이 새겨진 옥구슬의 위치 N 개가 주어질 때에, 임씨에게 돌아갈 대지의 넓이를 계산하는 프로그램을 작성하시오. 단, 옥구슬의 위치는 2 차원 정수 좌표로 주어지고 옥구슬은 같은 위치에 여러 개가 발견될 수도 있으며, x 축의 양의방향을 동쪽, y 축의 양의방향을 북쪽이라고 가정한다.
예를 들어 위와 같이 (2, 1), (3, 2), (5, 2), (3, 4) 네 점에서 옥구슬을 발견하였다면, 임씨에게 돌아갈 대지는 (2, 1), (5, 1), (2, 4), (5, 4)를 네 꼭짓점으로 하는 직사각형이며, 넓이는 (5 - 2) × (4 - 1) = 9 가 된다.
입력
첫째 줄에는 점의 개수 N (1 ≤ N ≤ 100,000) 이 주어진다. 이어지는 N 줄에는 각 점의 좌표가 두 개의 정수로 한 줄에 하나씩 주어진다. 각각의 좌표는 -10,000 이상 10,000 이하의 정수이다.
출력
첫째 줄에 N 개의 점을 둘러싸는 최소 크기의 직사각형의 넓이를 출력하시오.
문제 풀이 1
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int count = Integer.parseInt(br.readLine());
if (count == 1) {
System.out.println(0);
return;
}
int minX = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int minY = Integer.MAX_VALUE;
int maxY = Integer.MIN_VALUE;
for (int i = 0; i < count; i++) {
String[] splitStr = br.readLine().split(" ");
int x = Integer.parseInt(splitStr[0]);
int y = Integer.parseInt(splitStr[1]);
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
}
System.out.println((maxX - minX) * (maxY - minY));
}
}
문제 풀이 2 (stream 풀이방식은 성능저하가 있을수있다.)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int count = Integer.parseInt(br.readLine());
if (count == 1) {
System.out.println(0);
return;
}
int[] xArray = new int[count];
int[] yArray = new int[count];
for (int i = 0; i < count; i++) {
String[] splitStr = br.readLine().split(" ");
xArray[i] = Integer.parseInt(splitStr[0]);
yArray[i] = Integer.parseInt(splitStr[1]);
}
int minX = Arrays.stream(xArray).min().getAsInt();
int minY = Arrays.stream(yArray).min().getAsInt();
int maxX = Arrays.stream(xArray).max().getAsInt();
int maxY = Arrays.stream(yArray).max().getAsInt();
System.out.println((maxX - minX) * (maxY - minY));
}
}
-
-
-
[baekjoon] 직사각형에서 탈출 (백준 1085 java 풀이)
intro : Math 클래스의 min 메소드를 활용해서 로직 구성이 포인트
백준 문제링크
문제
한수는 지금 (x, y)에 있다. 직사각형은 각 변이 좌표축에 평행하고, 왼쪽 아래 꼭짓점은 (0, 0), 오른쪽 위 꼭짓점은 (w, h)에 있다. 직사각형의 경계선까지 가는 거리의 최솟값을 구하는 프로그램을 작성하시오.
입력
첫째 줄에 x, y, w, h가 주어진다.
출력
첫째 줄에 문제의 정답을 출력한다.
제한
1 ≤ w, h ≤ 1,000
1 ≤ x ≤ w-1
1 ≤ y ≤ h-1
x, y, w, h는 정수
문제 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int value1 = Integer.parseInt(st.nextToken());
int value2 = Integer.parseInt(st.nextToken());
int value3 = Integer.parseInt(st.nextToken());
int value4 = Integer.parseInt(st.nextToken());
int[] array = new int[4];
array[0] = value1;
array[1] = value2;
array[2] = Math.abs(value3 - value1);
array[3] = Math.abs(value4 - value2);
System.out.println(Math.min(array[0], Math.min(array[1], Math.min(array[2], array[3]))));
}
}
-
-
-
-
-
-
-
[baekjoon] 소수 (백준 2581 java 풀이)
intro : 소수를 찾고 소수의 합을 구하고, 첫번째 소수는 boolean 타입의 flag 값으로 체크하여 로직을 구성하는게 포인트
백준 문제링크
문제
자연수 M과 N이 주어질 때 M이상 N이하의 자연수 중 소수인 것을 모두 골라 이들 소수의 합과 최솟값을 찾는 프로그램을 작성하시오. 예를 들어 M=60, N=100인 경우 60이상 100이하의 자연수 중 소수는 61, 67, 71, 73, 79, 83, 89, 97 총 8개가 있으므로, 이들 소수의 합은 620이고, 최솟값은 61이 된다.
입력
입력의 첫째 줄에 M이, 둘째 줄에 N이 주어진다. M과 N은 10,000이하의 자연수이며, M은 N보다 작거나 같다.
출력
M이상 N이하의 자연수 중 소수인 것을 모두 찾아 첫째 줄에 그 합을, 둘째 줄에 그 중 최솟값을 출력한다. 단, M이상 N이하의 자연수 중 소수가 없을 경우는 첫째 줄에 -1을 출력한다.
문제 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
boolean isFirstPrice = false;
int firstPrice = 0;
int primeSum = 0;
int first = Integer.parseInt(br.readLine());
int second = Integer.parseInt(br.readLine());
for (int i = first; i <= second; i++) {
if (isPrime(i)) {
if (!isFirstPrice) {
firstPrice = i;
isFirstPrice = true;
}
primeSum += i;
}
}
if (primeSum == 0) {
System.out.println(-1);
} else {
System.out.println(primeSum);
System.out.println(firstPrice);
}
}
public static boolean isPrime(int number) {
if (number == 1) {
return false;
} else {
for (int i = 2; i < number; i++) {
if (number % i == 0) {
return false;
}
}
return true;
}
}
}
-
-
-
-
[diary] 깃꾸에 이어서 블로그 꾸미기 까지
intro : 나 HTML/CSS/JS 진짜 잘 모른단 말이야. 지킬테마가 다해주는거 아니었어 ?
깃꾸를 어느덧 마무리를 할때즈음.. 초기 깃허브 블로그의 디자인이 뭔가 마음에 들지 않았다고 생각했었는데 스물스물 이번 주말을 맞이하여 건들기 시작했다. 여기저기 바꾸느라고 기존의 있던 블로그의 디자인을 기록해 두지 못한게 조금 아쉬운데, 여튼 현재는 많은 부분들이 바뀌었다. 예를테면 좌측화면의 카테고리 부분의 색상이라던지 코드 블록의 사파리 테마적용, 폰트, 문장간의 간격등 미세하지만 눈에 거슬리는 부분들을 어느정도 수정하였다.
하지만 아직도 거슬리는 부분이 남아 있으니,,, 다크모드를 삭제할지 고민이 된다. 기본적으로 지킬테마에서 제공해주던 기능인데, 다크모드에서의 블로그 화면이 너무 별로여서 나중에 저 자리에 내가 내년에 진행할 프로젝트 사이트를 링크를 걸어둘까..도 생각중이다. 근데 또 다시 생각해보면 현재 다크모드가 있는 자리의 버튼은 이 블로그의 기능을 이용함에 있어서 부가적으로 기능을 제공해주는 역할의 무언가가 있기에 딱인 위치인데, 흠 뭘 더 추가하면 좋을까 고민이된다.
(공부는 안하고 맨날 꾸미느라 바쁘기만 하다.)
사실 이런 꾸미는데 시간을 소요할 시간이 많이 없음에도 불구하고, 나란 사람의 성격이 뭔가 하기전에 이쁘고 간지나고 트렌디하지못하면 그 공간에서 그 영역에서 뭘 하기가 싫다. 이상하게 이뻐야 관심이 가고 멋있어야 내가 그걸하고싶다. 왜인지는 모르겠다. 사실 깃블로그를 시작하게된 계기도 멋이 있어서 시작했던건데 블로그 디자인이 별로면 글을 쓰고싶지 않을거 같다는 생각에 꾸미는데에 엄청 공을 들이는거 같다. 하지만 꾸미는게 크게 티가나지는 않아살짝 아쉽다..ㅎ
이번주에 다음달에 예정된 리눅스 시험 신청을 했다. 이번년도가 가기전에 무언가 성취를 하면 나중에 24년도를 돌아보았을때 성취감이 느껴질거 같고, 리눅스는 서버개발자 백엔드 개발자로서 기본적인 덕목이라고 생각하기에 공부가 필요하다고 생각했는데 겸사겸사 자격증도 취득하면 좋을듯 하여 신청하였다. 작년에 공부안하고 시험만 보러갔었는데 그때 그냥 열심히 했으면 지금 정처기 공부를 하고 있었을텐데 이번년도에는 꼭 따리라 야무지게 다짐중이다.
(하지만 아직 공부는 시작도 안했다. 그게문제다. 인생...)
오늘까지만 놀고, 내일부터는 계획된 일정에 따라 하루를 살아가야할거 같다. 한동안 다이어리를 쓰는 시간보단 공부한 내용을 정리하고 기록하는 글을 쓰는데 더 초점을 두거나 시간을 투자할거 같다. 지금 까지 미뤄왔던 Java/Spring/Db/Jpa/Git 등 다양한 글의 쓸거리 주제들이 있는데 하나도 안쓰고 있는 날 보니 반성해야 겠다고 생각이 든다. 특히 Java/Git 쪽은 정말 쓸 내용이 산더미처럼 밀려있다. 얼른 정리하고 기록해야지.
(위 내용 다 거짓말. 사실 진짜 내마음 짤.)
-
-
[baekjoon] 세탁소 사장 동혁 (백준 2720 java 풀이)
intro : 1달러가 100센트인점을 생각한다면, 주어지는 값이 센트단위로 들어오니, 각 값을 25,10,5,1 로 나누어 몫과 나머지를 통해 로직을 구성하는게 포인트
백준 문제링크
문제
미국으로 유학간 동혁이는 세탁소를 운영하고 있다. 동혁이는 최근에 아르바이트로 고등학생 리암을 채용했다.동혁이는 리암에게 실망했다. 리암은 거스름돈을 주는 것을 자꾸 실수한다. 심지어 $0.5달러를 줘야하는 경우에 거스름돈으로 $5달러를 주는것이다! 어쩔수 없이 뛰어난 코딩 실력을 발휘해 리암을 도와주는 프로그램을 작성하려고 하지만, 디아블로를 하느라 코딩할 시간이 없어서 이 문제를 읽고 있는 여러분이 대신 해주어야 한다.거스름돈의 액수가 주어지면 리암이 줘야할 쿼터(Quarter, $0.25)의 개수, 다임(Dime, $0.10)의 개수, 니켈(Nickel, $0.05)의 개수, 페니(Penny, $0.01)의 개수를 구하는 프로그램을 작성하시오. 거스름돈은 항상 $5.00 이하이고, 손님이 받는 동전의 개수를 최소로 하려고 한다. 예를 들어, $1.24를 거슬러 주어야 한다면, 손님은 4쿼터, 2다임, 0니켈, 4페니를 받게 된다.
입력
첫째 줄에 테스트 케이스의 개수 T가 주어진다. 각 테스트 케이스는 거스름돈 C를 나타내는 정수 하나로 이루어져 있다. C의 단위는 센트이다. (1달러 = 100센트) (1<=C<=500)
출력
각 테스트케이스에 대해 필요한 쿼터의 개수, 다임의 개수, 니켈의 개수, 페니의 개수를 공백으로 구분하여 출력한다.
문제 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int forCount = Integer.parseInt(br.readLine());
for (int i = 0; i < forCount; i++) {
int[] result = new int[4];
double doubleValue = Double.parseDouble(br.readLine());
int intValue = (int) doubleValue / 25;
result[0] = intValue;
doubleValue %= 25;
if (doubleValue != 0) {
intValue = (int) doubleValue / 10;
result[1] = intValue;
doubleValue %= 10;
if (doubleValue != 0) {
intValue = (int) doubleValue / 5;
result[2] = intValue;
doubleValue %= 5;
if (doubleValue != 0) {
result[3] = (int) doubleValue;
}
}
}
for (int value : result) {
System.out.print(value + " ");
}
System.out.println();
}
}
}
-
-
[baekjoon] 세로읽기 (백준 10798 java 풀이)
intro : 세로로 읽는법은 단순히 열을 기준으로 반복문을 구성하여 1열 에대한 반복문 처리후, 2열에 대한 반복문 처리를 반복한다.
백준 문제링크
문제
아직 글을 모르는 영석이가 벽에 걸린 칠판에 자석이 붙어있는 글자들을 붙이는 장난감을 가지고 놀고 있다. 이 장난감에 있는 글자들은 영어 대문자 ‘A’부터 ‘Z’, 영어 소문자 ‘a’부터 ‘z’, 숫자 ‘0’부터 ‘9’이다.영석이는 칠판에 글자들을 수평으로 일렬로 붙여서 단어를 만든다. 다시 그 아래쪽에 글자들을 붙여서 또 다른 단어를 만든다. 이런 식으로 다섯 개의 단어를 만든다. 아래 그림 1은 영석이가 칠판에 붙여 만든 단어들의 예이다.
A A B C D D
a f z z
0 9 1 2 1
a 8 E W g 6
P 5 h 3 k x
<그림 1>
한 줄의 단어는 글자들을 빈칸 없이 연속으로 나열해서 최대 15개의 글자들로 이루어진다. 또한 만들어진 다섯 개의 단어들의 글자 개수는 서로 다를 수 있다. 심심해진 영석이는 칠판에 만들어진 다섯 개의 단어를 세로로 읽으려 한다. 세로로 읽을 때, 각 단어의 첫 번째 글자들을 위에서 아래로 세로로 읽는다. 다음에 두 번째 글자들을 세로로 읽는다. 이런 식으로 왼쪽에서 오른쪽으로 한 자리씩 이동 하면서 동일한 자리의 글자들을 세로로 읽어 나간다. 위의 그림 1의 다섯 번째 자리를 보면 두 번째 줄의 다섯 번째 자리의 글자는 없다. 이런 경우처럼 세로로 읽을 때 해당 자리의 글자가 없으면, 읽지 않고 그 다음 글자를 계속 읽는다. 그림 1의 다섯 번째 자리를 세로로 읽으면 D1gk로 읽는다. 그림 1에서 영석이가 세로로 읽은 순서대로 글자들을 공백 없이 출력하면 다음과 같다 ‘Aa0aPAf985Bz1EhCz2W3D1gkD6x’ 칠판에 붙여진 단어들이 주어질 때, 영석이가 세로로 읽은 순서대로 글자들을 출력하는 프로그램을 작성하시오.
입력
총 다섯줄의 입력이 주어진다. 각 줄에는 최소 1개, 최대 15개의 글자들이 빈칸 없이 연속으로 주어진다. 주어지는 글자는 영어 대문자 ‘A’부터 ‘Z’, 영어 소문자 ‘a’부터 ‘z’, 숫자 ‘0’부터 ‘9’ 중 하나이다. 각 줄의 시작과 마지막에 빈칸은 없다.
출력
영석이가 세로로 읽은 순서대로 글자들을 출력한다. 이때, 글자들을 공백 없이 연속해서 출력한다.
문제 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
String[][] result = new String[5][15];
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
for (int i = 0; i < 5; i++) {
String input = br.readLine();
String[] split = input.split("");
for (int j = 0; j < split.length; j++) {
result[i][j] = split[j];
}
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 15; i++) {
for (int j = 0; j < 5; j++) {
String str = result[j][i];
if (str != null) {
sb.append(str);
}
}
}
System.out.println(sb);
}
}
-
-
-
[baekjoon] 너의 평점은 (백준 25206 java 풀이)
intro : P학점에 대한 처리는 미처리 한다는 점을 주의해야하며, Map을 통해 학점의 대한 점수를 관리하는 방법이 포인트
백준 문제링크
문제
인하대학교 컴퓨터공학과를 졸업하기 위해서는, 전공평점이 3.3 이상이거나 졸업고사를 통과해야 한다. 그런데 아뿔싸, 치훈이는 깜빡하고 졸업고사를 응시하지 않았다는 사실을 깨달았다! 치훈이의 전공평점을 계산해주는 프로그램을 작성해보자. 전공평점은 전공과목별 (학점 × 과목평점)의 합을 학점의 총합으로 나눈 값이다 인하대학교 컴퓨터공학과의 등급에 따른 과목평점은 다음 표와 같다.
제목
내용
A+
4.5
A0
4.0
B+
3.5
B0
3.0
C+
2.5
C0
2.0
D+
1.5
D0
1.0
F
0.0
P/F 과목의 경우 등급이 P또는 F로 표시되는데, 등급이 P인 과목은 계산에서 제외해야 한다. 과연 치훈이는 무사히 졸업할 수 있을까?
입력
20줄에 걸쳐 치훈이가 수강한 전공과목의 과목명, 학점, 등급이 공백으로 구분되어 주어진다.
출력
치훈이의 전공평점을 출력한다. 정답과의 절대오차 또는 상대오차가 $10^-4$ 이하이면 정답으로 인정한다.
문제 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) throws IOException {
Map<String, Double> gradeMap = new HashMap<>();
gradeMap.put("A+", 4.5);
gradeMap.put("A0", 4.0);
gradeMap.put("B+", 3.5);
gradeMap.put("B0", 3.0);
gradeMap.put("C+", 2.5);
gradeMap.put("C0", 2.0);
gradeMap.put("D+", 1.5);
gradeMap.put("D0", 1.0);
gradeMap.put("F", 0.0);
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
double specificCredit = 0;
double totalCredit = 0;
for (int i = 0; i < 20; i++) {
String[] splitStr = br.readLine().split(" ");
double credit = Double.parseDouble(splitStr[1]);
String grade = splitStr[2];
if (!grade.equals("P")) {
totalCredit += credit;
specificCredit += (gradeMap.get(grade) * credit);
}
}
System.out.println(specificCredit / totalCredit);
}
}
-
-
[baekjoon] 그룹 단어 체커 (백준 1316 java 풀이)
intro : char 타입의 변수 - char 타입의 변수를 통해 int형을 반환한다는 점, 기존에 출력되었던 단어인지 확인하기위한 boolean 타입의 배열이 포인트
백준 문제링크
문제
그룹 단어란 단어에 존재하는 모든 문자에 대해서, 각 문자가 연속해서 나타나는 경우만을 말한다. 예를 들면, ccazzzzbb는 c, a, z, b가 모두 연속해서 나타나고, kin도 k, i, n이 연속해서 나타나기 때문에 그룹 단어이지만, aabbbccb는 b가 떨어져서 나타나기 때문에 그룹 단어가 아니다. 단어 N개를 입력으로 받아 그룹 단어의 개수를 출력하는 프로그램을 작성하시오.
입력
첫째 줄에 단어의 개수 N이 들어온다. N은 100보다 작거나 같은 자연수이다. 둘째 줄부터 N개의 줄에 단어가 들어온다. 단어는 알파벳 소문자로만 되어있고 중복되지 않으며, 길이는 최대 100이다.
출력
첫째 줄에 그룹 단어의 개수를 출력한다.
문제 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
int groupWordCount = 0;
for (int i = 0; i < n; i++) {
String word = br.readLine();
if (isGroupWord(word)) {
groupWordCount++;
}
}
System.out.println(groupWordCount);
}
public static boolean isGroupWord(String word) {
boolean[] visited = new boolean[26];
char prevChar = word.charAt(0);
visited[prevChar - 'a'] = true;
for (int i = 1; i < word.length(); i++) {
char currentChar = word.charAt(i);
if (prevChar != currentChar) {
if (visited[currentChar - 'a']) {
return false;
}
visited[prevChar - 'a'] = true;
}
prevChar = currentChar;
}
return true;
}
}
-
[baekjoon] 크로아티아 알파벳 (백준 2941 java 풀이)
intro : replace 메소드를 통해 단순히 크로아티아의 문자를 “ “처리하고 나중에 문자열 길이를 계산하는게 포인트
백준 문제링크
문제
예전에는 운영체제에서 크로아티아 알파벳을 입력할 수가 없었다. 따라서, 다음과 같이 크로아티아 알파벳을 변경해서 입력했다. 예를 들어, ljes=njak은 크로아티아 알파벳 6개(lj, e, š, nj, a, k)로 이루어져 있다. 단어가 주어졌을 때, 몇 개의 크로아티아 알파벳으로 이루어져 있는지 출력한다. dž는 무조건 하나의 알파벳으로 쓰이고, d와 ž가 분리된 것으로 보지 않는다. lj와 nj도 마찬가지이다. 위 목록에 없는 알파벳은 한 글자씩 센다.
입력
첫째 줄에 최대 100글자의 단어가 주어진다. 알파벳 소문자와 ‘-‘, ‘=’로만 이루어져 있다.단어는 크로아티아 알파벳으로 이루어져 있다. 문제 설명의 표에 나와있는 알파벳은 변경된 형태로 입력된다.
출력
입력으로 주어진 단어가 몇 개의 크로아티아 알파벳으로 이루어져 있는지 출력한다.
문제 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
String[] strArray = new String[]{"c=", "c-", "dz=", "d-", "lj", "nj", "s=", "z="};
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String input = br.readLine();
for (String str : strArray) {
input = input.replaceAll(str, " ");
}
System.out.println(input.length());
}
}
-
-
[baekjoon] 바구니 뒤집기 (백준 10811 java 풀이)
intro : 지정된 범위의 값을 어떻게 역순으로 변환할것인지 로직을 구성하는게 포인트, 첫번쨰값과 마지막값을 바꾼다는 개념에서 시작해야한다는 점.
백준 문제링크
문제
도현이는 바구니를 총 N개 가지고 있고, 각각의 바구니에는 1번부터 N번까지 번호가 순서대로 적혀져 있다.바구니는 일렬로 놓여져 있고, 가장 왼쪽 바구니를 1번째 바구니, 그 다음 바구니를 2번째 바구니, …, 가장 오른쪽 바구니를 N번째 바구니라고 부른다. 도현이는 앞으로 M번 바구니의 순서를 역순으로 만들려고 한다. 도현이는 한 번 순서를 역순으로 바꿀 때, 순서를 역순으로 만들 범위를 정하고, 그 범위에 들어있는 바구니의 순서를 역순으로 만든다. 바구니의 순서를 어떻게 바꿀지 주어졌을 때, M번 바구니의 순서를 역순으로 만든 다음, 바구니에 적혀있는 번호를 가장 왼쪽 바구니부터 출력하는 프로그램을 작성하시오.
입력
첫째 줄에 N (1 ≤ N ≤ 100)과 M (1 ≤ M ≤ 100)이 주어진다. 둘째 줄부터 M개의 줄에는 바구니의 순서를 역순으로 만드는 방법이 주어진다. 방법은 i j로 나타내고, 왼쪽으로부터 i번째 바구니부터 j번째 바구니의 순서를 역순으로 만든다는 뜻이다. (1 ≤ i ≤ j ≤ N) 도현이는 입력으로 주어진 순서대로 바구니의 순서를 바꾼다.
출력
모든 순서를 바꾼 다음에, 가장 왼쪽에 있는 바구니부터 바구니에 적혀있는 순서를 공백으로 구분해 출력한다.
문제 풀이
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String input = sc.nextLine();
String[] inputSplit = input.split(" ");
int arrLength = Integer.parseInt(inputSplit[0]);
int forCount = Integer.parseInt(inputSplit[1]);
int[] result = new int[arrLength];
for (int i = 0; i < arrLength; i++) {
result[i] = i + 1;
}
for (int i = 0; i < forCount; i++) {
String line = sc.nextLine();
String[] lineSplit = line.split(" ");
int firstIndex = Integer.parseInt(lineSplit[0]) - 1;
int secondIndex = Integer.parseInt(lineSplit[1]) - 1;
for (int j = firstIndex; j <= secondIndex; j++) {
int temp = result[j];
result[j] = result[secondIndex];
result[secondIndex] = temp;
secondIndex--;
}
}
sc.close();
for (int m : result) {
System.out.print(m + " ");
}
}
}
-
-
-
-
[baekjoon] 킹, 퀸, 룩, 비숍, 나이트, 폰 (백준 3003 java 풀이)
intro : 존재해야하는 체스말의 값을 배열로 선언하고, 입력받은 값과 비교해서 -+ 값을 구하는 문제
백준 문제링크
문제
동혁이는 오래된 창고를 뒤지다가 낡은 체스판과 피스를 발견했다. 체스판의 먼지를 털어내고 걸레로 닦으니 그럭저럭 쓸만한 체스판이 되었다. 하지만, 검정색 피스는 모두 있었으나, 흰색 피스는 개수가 올바르지 않았다. 체스는 총 16개의 피스를 사용하며, 킹 1개, 퀸 1개, 룩 2개, 비숍 2개, 나이트 2개, 폰 8개로 구성되어 있다. 동혁이가 발견한 흰색 피스의 개수가 주어졌을 때, 몇 개를 더하거나 빼야 올바른 세트가 되는지 구하는 프로그램을 작성하시오.
입력
첫째 줄에 동혁이가 찾은 흰색 킹, 퀸, 룩, 비숍, 나이트, 폰의 개수가 주어진다. 이 값은 0보다 크거나 같고 10보다 작거나 같은 정수이다.
출력
첫째 줄에 입력에서 주어진 순서대로 몇 개의 피스를 더하거나 빼야 되는지를 출력한다. 만약 수가 양수라면 동혁이는 그 개수 만큼 피스를 더해야 하는 것이고, 음수라면 제거해야 하는 것이다.
문제 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
StringBuilder sb = new StringBuilder();
int[] value = new int[]{1, 1, 2, 2, 2, 8};
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String[] splitStr = br.readLine().split(" ");
for (int i = 0; i < splitStr.length; i++) {
sb.append(value[i] - Integer.parseInt(splitStr[i])).append(" ");
}
System.out.println(sb);
}
}
-
-
-
-
-
-
-
-
-
-
-
[baekjoon] 공 넣기 (백준 10810 java 풀이)
intro : 주어진 인덱스 특정 범위에 값을 넣는것을 반복하고 공이들어있는 곳에 1값을 넣어 출력하는게 포인트
백준 문제링크
문제
도현이는 바구니를 총 N개 가지고 있고, 각각의 바구니에는 1번부터 N번까지 번호가 매겨져 있다. 또, 1번부터 N번까지 번호가 적혀있는 공을 매우 많이 가지고 있다. 가장 처음 바구니에는 공이 들어있지 않으며, 바구니에는 공을 1개만 넣을 수 있다. 도현이는 앞으로 M번 공을 넣으려고 한다. 도현이는 한 번 공을 넣을 때, 공을 넣을 바구니 범위를 정하고, 정한 바구니에 모두 같은 번호가 적혀있는 공을 넣는다. 만약, 바구니에 공이 이미 있는 경우에는 들어있는 공을 빼고, 새로 공을 넣는다. 공을 넣을 바구니는 연속되어 있어야 한다. 공을 어떻게 넣을지가 주어졌을 때, M번 공을 넣은 이후에 각 바구니에 어떤 공이 들어 있는지 구하는 프로그램을 작성하시오.
입력
첫째 줄에 N (1 ≤ N ≤ 100)과 M (1 ≤ M ≤ 100)이 주어진다. 둘째 줄부터 M개의 줄에 걸쳐서 공을 넣는 방법이 주어진다. 각 방법은 세 정수 i j k로 이루어져 있으며, i번 바구니부터 j번 바구니까지에 k번 번호가 적혀져 있는 공을 넣는다는 뜻이다. 예를 들어, 2 5 6은 2번 바구니부터 5번 바구니까지에 6번 공을 넣는다는 뜻이다. (1 ≤ i ≤ j ≤ N, 1 ≤ k ≤ N) 도현이는 입력으로 주어진 순서대로 공을 넣는다.
출력
1번 바구니부터 N번 바구니에 들어있는 공의 번호를 공백으로 구분해 출력한다. 공이 들어있지 않은 바구니는 0을 출력한다.
문제 풀이
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String input = sc.nextLine();
String[] inputSplit = input.split(" ");
int arrLength = Integer.parseInt(inputSplit[0]);
int forCount = Integer.parseInt(inputSplit[1]);
int[] result = new int[arrLength];
for (int i = 0; i < forCount; i++) {
input = sc.nextLine();
inputSplit = input.split(" ");
int value1 = Integer.parseInt(inputSplit[0]);
int value2 = Integer.parseInt(inputSplit[1]);
int value3 = Integer.parseInt(inputSplit[2]);
for (int j = value1 - 1; j < value2; j++) {
result[j] = value3;
}
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < result.length; i++) {
sb.append(result[i]);
if (!(i == (result.length - 1))) {
sb.append(" ");
}
}
System.out.println(sb);
}
}
-
-
-
[baekjoon] 영수증 (백준 25304 java 풀이)
intro : 물품의 가격과 수량의 개수를 계산한 각 라인의 총합과, 영수증의 가격을 비교해서 일치/불일치 여부를 처리하는게 포인트
백준 문제링크
문제 설명
준원이는 저번 주에 살면서 처음으로 코스트코를 가 봤다. 정말 멋졌다. 그런데, 몇 개 담지도 않았는데 수상하게 높은 금액이 나오는 것이다! 준원이는 영수증을 보면서 정확하게 계산된 것이 맞는지 확인해보려 한다 영수증에 적힌, 구매한 각 물건의 가격과 개수 구매한 물건들의 총 금액 을 보고, 구매한 물건의 가격과 개수로 계산한 총 금액이 영수증에 적힌 총 금액과 일치하는지 검사해보자.
입력
첫째 줄에는 영수증에 적힌 총 금액 X가 주어진다. 둘째 줄에는 영수증에 적힌 구매한 물건의 종류의 수 N이 주어진다. 이후 N개의 줄에는 각 물건의 가격 a와 개수 b가 공백을 사이에 두고 주어진다.
출력
구매한 물건의 가격과 개수로 계산한 총 금액이 영수증에 적힌 총 금액과 일치하면 Yes를 출력한다. 일치하지 않는다면 No를 출력한다.
제한
1 ≤ X ≤ 1,000,000,000
1 ≤ N ≤ 100
1 ≤ a ≤ 1,000,000
1 ≤ b ≤ 10
문제 풀이
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int total = sc.nextInt();
sc.nextLine();
int count = sc.nextInt();
sc.nextLine();
int checkSum = 0;
for (int i = 0 ; i < count; i++) {
String input = sc.nextLine();
String[] splitInput = input.split(" ");
checkSum += (Integer.parseInt(splitInput[0]) * Integer.parseInt(splitInput[1]));
}
if (total == checkSum) {
System.out.println("Yes");
} else {
System.out.println("No");
}
}
}
문제 해석
단순히 Scanner를 이용해서 값을 받아서 입력받은 값이 실제 영수증에 입력된 값과 같은지 비교하는 로직을 구성하는 문제, 중요한 포인트는 정수형 타입을 Scanner로 받은다음에 sc.nextLine을 해야한다는 점 이부분을 하지않는다면 Exception 발생
Scanner sc = new Scanner(System.in);
int total = sc.nextInt();
sc.nextLine();
int count = sc.nextInt();
sc.nextLine();
-
-
-
-
-
[programmers] 중복된 숫자 개수 (프로그래머스 java 풀이)
intro : 배열안에 값을 반복문으로 하나씩 비교해서 일치하는 개수를 찾으면 된다.
프로그래머스 문제링크
문제 설명
정수가 담긴 배열 array와 정수 n이 매개변수로 주어질 때, array에 n이 몇 개 있는 지를 return
하도록 solution 함수를 완성해보세요.
제한사항
1 ≤ array의 길이 ≤ 100
0 ≤ array의 원소 ≤ 1,000
0 ≤ n ≤ 1,000
입출력 예
array
n
result
[1, 1, 2, 3, 4, 5]
1
2
[0, 2, 3, 4]
1
0
입출력 예 설명
입출력 예 #1
[1, 1, 2, 3, 4, 5] 에는 1이 2개 있습니다.
입출력 예 #2
[0, 2, 3, 4] 에는 1이 0개 있습니다.
문제 풀이
public int solution(int[] array, int n) {
// return 할 변수 선언
int answer = 0;
// 향상된 반복문을 실행
for (int i : array) {
// 찾고자 하는 값과 같은지 if 조건문을 통해 비교
if (i == n) {
// 만약 찾고자 하는 값과 같다면 + 1
answer += 1;
}
}
// 결과값 반환
return answer;
}
// 테스트 1 통과 (0.06ms, 73.8MB)
// 테스트 2 통과 (0.02ms, 78MB)
// 테스트 3 통과 (0.02ms, 72.8MB)
// 테스트 4 통과 (0.03ms, 77.6MB)
// 테스트 5 통과 (0.01ms, 74.7MB)
// 테스트 6 통과 (0.02ms, 75.3MB)
문제 해석
배열 array를 반복문을 통해 각각의 원소에 접근하여, 매개변수 n 의 값과 동일한 값이 존재하는지 비교하고 만약, 값이 일치하는경우 answer 변수에 +1 처리 후 반복문이 종료되는 시점에서 answer 변수를 return 한다
-
Touch background to close