Now Loading ...
-
-
-
🌐[Network] 네트워크 개념 - 프로토콜의 이해
🌐[Network] 네트워크 개념 - 프로토콜의 이해
Preview
데이터 통신의 개념과 프로토콜의 동작 원리를 이해하는 데 스마트폰은 많은 도움을 줍니다.
스마트폰으로 전화를 거는 사람(송신자)과 받는 사람(수신자)은 서로 합의하여 통화 연결을 설정합니다
데이터 전송 단계인 통화 과정에서는 묵시적인 규칙에 따라 대화를 주고받는 일련의 절차가 진행됩니다.
스마트폰은 양쪽이 동시에 말할 수 있는 양방향 통신기능을 지원하지만, 실제로 두 사람이 동시에 말하는 경우는 거의 없고, 느낌이나 말의 내용 등에 따라 번갈아가며 대화합니다.
상대방이 한 말을 이해하지 못했거나 제대로 듣지 못한 경우에는 다시 묻는 방식을 통하여 오류를 바로 잡기도 합니다.
이와 같은 대화 과정의 절차나 대화 내용을 올바르게 이해하는 과정이 모두 프로토콜의 기능에 포함됩니다.
네트워크에서 데이터 전송 원리도 이와 비슷합니다.
통신 프로토콜에서 중심적으로 다루는 내용은 주소 개념을 포함하여 데이터 전송 오류에 관한 오류 복구 기능, 전송 속도 조절에 관한 흐름 제어 기능, 데이터의 전달 경로에 관한 라우팅 기능입니다.
스마트폰을 사용한 대화 과정에서 사람들이 무의식적으로 행하는 통화 기법이 통신 프로토콜의 설계 과정에 그대로 반영되어 있습니다.
그러나 통화 과정은 간단해 보여도 이를 통신 프로토콜로 작성하는 일은 생각보다 쉽지 않습니다.
인터넷의 동작 원리를 이해하려면 계층 구조로 설계된 OSI 7계층 모델에 대한 할습이 선행되어야 합니다.
특히, 모듈화 설계 개념의 원리와 상하 계층의 역할 분담에 대한 학습을 통하여 계층별 프로토콜의 역할을 이해해야 합니다.
인터넷에서 사용되는 데이터 전송 프로토콜인 TCP, UDP, IP 프로토콜은 계층 구조 모델의 원리에 따라 설계되었으며, 기타 제어 프로토콜의 동작도 계층 구조의 원리에 따라 이루어집니다.
1️⃣ 프로토콜의 이해
네트워크에 연결된 시스템이 통신하려면 정해진 규칙에 따라 순차적으로 데이터를 주고 받아야 하는데, 이러한 일련의 규칙을 “프로토콜(Protocol)” 이라 합니다.
프로토콜의 동작 과정은 전송 오류 여부, 데이터 전달 경로, 전송 속도 등 다양한 외부 요인의 영향을 받습니다.
따라서 적절한 대응 방안을 마련해 효율적으로 관리해야 하는데, 프로토콜의 설계 과정은 모듈(Module)화를 통하여 이루어집니다.
이렇게 함으로써 시스템의 복잡성을 단순화하고, 사용자에게 더 편리하고 간편한 통신 기능을 제공할 수 있습니다.
1️⃣ 계층적 모듈 구조
일반적으로 복잡하고 큰 시스템의 기능은 특정 단위의 모듈로 나누어 설계합니다.
각각의 모듈은 독립적으로 동작하면서도 상호 유기적으로 통합될 수 있어야 합니다.
그러므로 모듈과 모듈을 서로 연동해 동작시키는 적절한 인터페이스가 필요합니다.
모듈화
컴퓨터를 하드웨어 측면에서 보면 CPU, 메모리, 하드디스크, LAN 카드 등과 같은 작은 부품들이 모여 하나의 시스템을 구성합니다.
복잡한 시스템을 기능별로 모듈화하면 시스템 구조가 단순해져서 전체 시스템을 이해하기 쉽습니다.
또한 각 단위 모듈이 독립적인 기능을 수행하기 때문에 고장이나 업그레이드 등의 상황에 손쉽게 대처할 수 있습니다.
소프트웨어 측명에서 보면, 일반 프로그래밍 언어에서는 함수의 개념을 사용해 전체 프로그램을 모듈화할 수 있습니다.
함수별로 특정 기능을 독립적으로 수행하도록 함으로써, 각 함수가 개별적으로 설계되고 구현된다는 장점이 있습니다.
함수 사이의 인터페이스는 함수의 매개변수에 의해서만 이루어지므로 전체 시스템을 이해하기가 훨씬 쉽습니다.
즉, 함수의 역할이 매개변수로 추상화되므로 내부 구조를 이해하지 않고도 함수들을 이해할 수 있고, 이들의 모임인 전체 시스템도 쉽게 이해할 수 있습니다.
위 그림은 시스템 모듈화의 장점을 보여줍니다.
(a)처럼 전체 시스템을 기능에 따라 세 부분으로 나누어 설계할 수 있는데, 이때 각 모듈은 정해진 인터페이스에 맞게 유기적으로 연결되어야 합니다.
B 모듈은 A 모듈과 곡선 모양의 인터페이스로 연결되고, C 모듈과는 톱니 모양의 인터페이스로 연결됩니다.
A와 C 모듈은 직접 연결되는 인터페이스가 없으며, B 모듈을 통해 간접적인 관계를 유지합니다.
위 그림의 시스템을 모듈화하지 않았다면 한 부분만 고장나도 전체 시스템을 교체해야 합니다.
반면 모듈화하여 설계하면 B 모듈에 대하여 (a)의 버전 1을 (b)의 버전 2로 교체하면 됩니다.
그림에서 설명한 것과 같이 B 모듈의 동작에 오류가 확인되었거나 기능이 개선된 경우 A 모듈이나 B 모듈과 관계없이 B 모듈만 수정하는 방식입니다.
이때 모듈 내부의 처리 과정은 임의로 개선할 수 있으나, A와 C 모듈 간 인터페이스는 동일하게 유지해야 전체 시스템 동작에 영향을 주지 않습니다.
계층 구조
분할된 모듈들은 협력 관계를 유지하면서 유기적으로 동작합니다.
모듈 구조는 서로 동등한 위치에서 서비스를 주고받을 수도 있고 특정 모듈이 다른 모듈에 서비스를 제공하는 형식의 계층 구조를 이루기도 합니다.
네트워크에서는 독립적인 고유 기능을 수행하는 모듈들이 상하 계층 구조로 연결되어 동작합니다.
계층 구조에서는 위 그림에서처럼 상위 계층이 하위 계층에 특정 서비스를 요청하는 방식으로 동작합니다(1).
요청을 받은 하위 계층은 해당 서비스를 실행하여 그 결과를 상위 계층에 돌려줍니다(2).
하위 계층의 실행 결과는 상위 계층에 결과 값을 직접 전달하는 방식이 될 수도 있고, 주변 환경 값을 변경하는 부수 효과(Side Effect) 방식일 수도 있습니다.
자동차의 경우를 예로 들어봅시다.
운전자가 자동차의 속도를 줄이려면 브레이크를 밟아야 하고 브레이크를 누르는 정도에 따라 속도가 줄어듭니다.
이 구조에서 운전자는 상위 계층에 해당되며, 자동차 내부에서 속도를 줄이는 기능은 하위 계층의 모듈이 됩니다.
운전자와 감속 모듈 사이에는 브레이크라는 인터페이스가 존재합니다.
브레이크 인터페이스가 정의되면 상위 계층의 운전자가 바뀌거나 하위 계층의 자동차가 바뀌어도 둘 사이의 서비스 개념을 유지할 수 있습니다.
네트워크에서 통신하는 시스템들은 계층 구조로 모듈화된 기능이 각각 동작하며, 둘 사이의 같은 계층끼리 데이터를 전달합니다.
이때 데이터를 전달하는 규칙을 프로토콜이라 합니다.
모듈화된 계층 구조 프로토콜에는 다양한 장점이 있지만, 대표적인 장점 몇 가지를 정리하면 다음과 같습니다.
복잡하고 큰 시스템을 기능별로 작게 분류해서 간단하고 작은 시스템으로 재구성할 수 있습니다.
따라서 전체 시스템을 이해하기 쉽고, 시스템을 설계하고 구현하기도 편리합니다.
상하 계층에 인접한 모듈 사이의 인터페이스를 포함하여 분할된 모듈이 연동할 수 있는 표준 인터페이스를 제공합니다.
모듈 인터페이스는 가능하면 단순하게 구현하여 모듈들이 최대한 독립적으로 동작하도록 해야합니다.
모듈의 독립성은 전체 시스템의 구조를 단순하게 만들어줍니다.
전송 매체 양단에 있는 호스트가 수행하는 프로토콜들은 좌우 대칭 구조입니다.
대칭 구조에서는 통신 양단에 위치하는 동일 계층 사이의 프로토콜을 단순화할 수 있습니다.
각 계층의 기능 오류를 수정하거나 향상시켜야 하는 경우에 전체 시스템을 재작성하지 않고 해당 계층의 모듈만 교체하면 됩니다.
즉, 상하 혹은 좌우 계층 간의 인터페이스를 유지하면 특정 계층의 내부 변경이 다른 모듈의 동작에 영향을 미치지 않습니다.
-
☕️[Java] 메서드 체이닝 - Method Chaining
☕️[Java] 메서드 체이닝 - Method Chaining.
간단한 예제 코드로 메서드 체이닝(Method Chaining)에 대해 알아봅시다.
1️⃣ 예제 코드.
public class ValueAdder {
private int value;
public ValueAdder add(int addValue) {
value += addValue;
return this;
}
public int getValue() {
return value;
}
}
단순히 값을 누적해서 더하는 기능을 제공하는 클래스입니다.
add() 메서드를 호출할 때 마다 내부의 value에 값을 누적합니다.
add() 메서드를 보면 자기 자신(this)의 참조값을 반환합니다.(이 부분을 유의해서 봅시다.)
public class MethodChainingMain1 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
adder.add(1);
adder.add(2);
adder.add(3);
int result = adder.getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
add() 메서드를 여러번 호출해서 값을 누적하여 더하고 출력합니다.
여기서는 add() 메서드의 반환값은 사용하지 않았습니다.
이번에는 add() 메서드의 반환값을 사용해봅시다.
public class MethodChainingMain2 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
ValueAdder adder1 = adder.add(1);
ValueAdder adder2 = adder1.add(2);
ValueAdder adder3 = adder2.add(3);
int result = adder3.getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
실행 결과는 기존과 같습니다.
adder.add(1) 을 호출합니다.
add() 메서드는 결과를 누적하고 자기 자신의 참조값인 this(x001)를 반환합니다.
adder1 변수는 adder와 같은 x001 인스턴스를 참조합니다.
add() 메서드는 자기 자신(this)의 참조값을 반환합니다.
이 반환값을 adder1, adder2, adder3에 보관했습니다.
따라서 adder, adder1, adder2, adder3은 모두 같은 참조값을 사용합니다.
왜냐하면 add() 메서드가 자기 자신(this)의 참조값을 반환했기 때문입니다.
그런데 이 방식은 처음 방식보다 더 불편하고, 코드도 잘 읽히지 않습니다.
이런 방식을 왜 사용할까요?
이번에는 방금 사용했던 방식에서 반환된 참조값을 새로운 변수에 담아서 보관하지 않고, 대신에 바로 메서드에 호출에 사용해보겠습니다.
public class MethodChainingMain3 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
int result = adder.add(1).add(2).add(3).getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
실행 순서
add() 메서드를 호출하면 ValueAdder 인스턴스 자신의 참조값(x001)이 반환됩니다.
이 반환된 참조값을 변수에 담아두지 않아도 됩니다.
대신에 반환된 참조값을 즉시 사용해서 바로 메서드를 호출할 수 있습니다.
다음과 같은 순서로 실행됩니다.
adder.add(1).add(2).add(3).getValue(); // value = 0
x001.add(1).add(2).add(3).getValue(); // value = 0, x001.add(1)을 호출하면 그 결과로 x001을 반환합니다.
x001.add(2).add(3).getValue(); // value = 1, x001.add(2)을 호출하면 그 결과로 x001을 반환합니다.
x001.add(3).getValue(); // value = 3, x001.add(3)을 호출하면 그 결과로 x001을 반환합니다.
x001.getValue(); // value = 6
6
메서드 호출의 결과로 자기 자신의 참조값을 반환하면, 반환된 참조값을 사용해서 메서드 호출을 계속 이어갈 수 있습니다.
코드를 보면 .을 찍고 메서드를 계속 연결해서 사용합니다.
마치 메서드가 체인으로 연결된 것 처럼 보입니다.
이러한 기법을 메서드 체이닝이라고 합니다.
물론 실행 결과도 기존과 동일합니다.
기존에는 메서드를 호출할 때 마다 계속 변수명에 .을 찍어야 했습니다
예) adder.add(1), adder.add(2)
메서드 체이닝 방식은 메서드가 끝나는 시점에 바로 . 을 찍어서 변수명을 생략할 수 있습니다.
메서드 체이닝이 가능한 이유는 자기 자신의 참조값을 반환하기 때문입니다.
이 참조값에 .을 찍어서 바로 자신의 메서드를 호출할 수 있습니다.
메서드 체이닝 기법은 코드를 간결하고 읽기 쉽게 만들어줍니다.
2️⃣ StringBuilder와 메서드 체인(Chain)
StringBuilder는 메서드 체이닝 기법을 제공합니다.
StringBuilder의 append() 메서드를 보면 자기 자신의 참조값을 반환합니다.
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuilder에서 문자열을 변경하는 대부분의 메서드도 메서드 체이닝 기법을 제공하기 위해 자기 자신을 반환합니다.
예) insert(), delete(), reverse()
public class StringBuilderMain1_1 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("A");
sb.append("B");
sb.append("C");
sb.append("D");
System.out.println("sb = " + sb);
sb.insert(4, "Java");
System.out.println("sb = " + sb);
sb.delete(4, 8);
System.out.println("delete = " + sb);
sb.reverse();
System.out.println("reverse = " + sb);
// StringBuild -> String
String string = sb.toString();
System.out.println("string = " + string);
}
}
위 코드를 메서드 체이닝 기법을 사용하면 아래와 같이 코드를 작성할 수 있습니다.
public class StringBuilderMain1_2 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
String string = sb.append("A").append("B").append("C").append("D")
.insert(4, "Java")
.delete(4, 8)
.reverse()
.toString();
System.out.println("string = " + string);
}
}
실행 결과
string = DCBA
3️⃣ 정리.
“만드는 사람이 수고로우면 쓰는 사람이 편하고, 만드는 사람이 편하면 쓰는 사람이 수고롭다” 는 말이 있습니다.
메서드 체이닝은 구현하는 입장에서는 번거롭지만 사용하는 개발자는 편리해집니다.
참고로 자바의 라이브러리와 오픈 소스들은 메서드 체이닝 방식을 종종 사용합니다.
-
☕️[Java] StringBuilder - 가변 String
☕️[Java] StringBuilder - 가변 String
1️⃣ 불변인 String 클래스의 단점.
불변인 String 클래스에도 단점이 있습니다.
다음 예시를 봅시다. (참고로 실제로 작동하는 코드는 아닙니다.)
"A" + "B"
String("A") + String("B") // 문자는 String 타입입니다.
String("A").concat(String("B")) // 문자의 더하기는 concat을 사용합니다.
new String("AB") // String은 불변입니다. 따라서 새로운 객체가 생성됩니다.
불변인 String의 내부 값은 변경할 수 없습니다.
따라서 변경된 값을 기반으로 새로운 String 객체를 생성합니다.
더 많은 문자를 더하는 경우를 살펴봅시다.
String str = "A" + "B" + "C" + "D";
String str = String("A") + String("B") + String("C") + String("D");
String str = new String("AB") + String("C") + String("D");
String str = new String("ABC") + String("D");
String str = new String("ABCD");
이 경우 총 3개의 String 클래스가 추가로 생성됩니다.
그런데 문제는 중간에 만들어진 new String("AB"), new String("ABC") 는 사용되지 않습니다. 최종적으로 만들어진 new String("ABCD") 만 사용됩니다.
결과적으로 중간에 만들어진 new String("AB"), new String("ABC)" 는 제대로 사용되지도 않고, 이후 GC의 대상이 됩니다.
불변인 String 클래스의 단점은 문자를 더하거나 변경할 때 마다 계속해서 새로운 객체를 생성해야 한다는 점 입니다.
문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은 String 객체를 만들고, GC해야 합니다.
결과적으로 컴퓨터의 CPU, 메모리 자원을 더 많이 사용하게 됩니다.
그리고 문자열의 크기가 클수록, 문자열을 더 자주 변경할수록 시스템의 자원을 더 많이 소모합니다.
참고: 실제로 문자열을 다룰 때 자바가 내부에서 최적화를 적용합니다.
2️⃣ StringBuilder
이 문제를 해결하는 방법은 단순합니다.
바로 불변이 아닌 가변 String이 존재하면 됩니다.
가변은 내부의 값을 바로 변경하면 되기 때문에 새로운 객체를 생성할 필요가 없습니다.
따라서 성능과 메모리 사용면에서 불변보다 더 효율적입니다.
이런 문제를 해결하기 위해 자바는 StringBuilder 라는 가변 String을 제공합니다.
물론 가변의 경우 사이드 이펙트에 주의해서 사용해야 합니다.
StringBuilder는 내부에 final이 아닌 변경할 수 있는 byte[]을 가지고 있습니다.
public final class StringBuilder {
char[] value; // 자바 9 이전
byte[] value; // 자바 9 이후
// 여러 메서드
public StringBuilder append(String str) {...}
public int length() {...}
...
}
(실제로는 상속 관계에 있고 부모 클래스인 AbstractStringBuilder에 value 속성과 length() 메서드가 있습니다.)
3️⃣ StringBuilder 사용 예시
public class StringBuilderMain1_1 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("A");
sb.append("B");
sb.append("C");
sb.append("D");
System.out.println("sb = " + sb);
sb.insert(4, "Java");
System.out.println("sb = " + sb);
sb.delete(4, 8);
System.out.println("delete = " + sb);
sb.reverse();
System.out.println("reverse = " + sb);
// StringBuild -> String
String string = sb.toString();
System.out.println("string = " + string);
}
}
StringBuilder 객체를 생성합니다.
append() 메서드를 사용해 여러 문자열을 추가합니다.
insert() 메서드로 특정 위치에 문자열을 삽입합니다.
delete() 메서드로 특정 범위의 문자열을 삭제합니다.
reverse() 메서드로 문자열을 뒤집습니다.
마지막으로 toString 메소드를 사용해 StringBuilder의 결과를 기반으로 String을 생성해서 반환합니다.
실행 결과
sb = ABCD
sb = ABCDJava
delete = ABCD
reverse = DCBA
string = DCBA
가변(Mutable) vs 불변(Immutable)
String은 불변합니다.
즉, 한 번 생성되면 그 내용을 변경할 수 없습니다.
따라서 문자열에 변화를 주려고 할 때마다 새로운 String 객체가 생성되고, 기존 객체는 버려집니다.
이 과정에서 메모리와 처리 시간을 더 많이 소모합니다.
반면에, StringBuilder는 가변적입니다.
하나의 StringBuilder 객체 안에서 문자열을 추가, 삭제, 수정할 수 있으며, 이 때마다 새로운 객체를 생성하지 않습니다.
이로 인해 메모리 사용을 줄이고 성능을 향상시킬 수 있습니다.
단 사이드 이펙트를 주의해야합니다.
StringBuilder는 보통 문자열을 변경하는 동안만 사용하다가 문자열 변경이 끝나면 안전한(불변) String으로 변환하는 것이 좋습니다.
-
☕️[Java] String 최적화
☕️[Java] String 최적화
1️⃣ 자바의 String 최적화.
자바 컴파일러는 다음과 같이 문자열 리터럴을 더하는 부분을 자동을 합쳐줍니다.
문자열 리터럴 최적화.
컴파일 전
String helloWorld = "Hello, " + "World!";
컴파일 후
String helloWorld = "Hello, World!";
따라서 런타임에 별도의 문자열 결합 연산을 수행하지 않기 때문에 성능이 향상됩니다.
String 변수 최적화
문자열 변수의 경우 그 안에 어떤 값이 들어있는지 컴파일 시점에는 알 수 없기 때문에 단순하게 합칠 수 없습니다.
String result = str1 + str2;
이런 경우 예를 들면 다음과 같이 최적화를 수행합니다
(최적화 방식은 자바 버전에 따라 달라집니다.)
String result = new StringBuilder().append(str1).append(str2).toString();
참고: 자바 9부터는 StringConcatFactory를 사용해서 최적화를 수행합니다.
이렇듯 자바가 최적화를 처리해주기 때문에 지금처럼 간단한 경우에는 StringBuilder를 사용하지 않아도 됩니다.
대신에 문자열 더하기 연산(+)을 사용하면 충분합니다.
2️⃣ String 최적화가 어려운 경우.
다음과 같이 문자열을 루프안에서 문자열을 더하는 경우에는 최적화가 이루어지지 않습니다.
public class LoopStringMain {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
String result = "";
for (int i = 0; i < 100000; i++) {
result += "Hello Java ";
}
long endTime = System.currentTimeMillis();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
왜냐하면 대략 다음과 같이 최적화가 되기 때문이다.(최적화 방식은 자바 버전에 따라 다릅니다.)
String result = "";
for (int i = 0; i < 1000000; i++) {
result = new StringBuilder().append(result).append("Hello Java ").toString();
}
반복문의 루프 내부에서는 최적화가 되는 것 처럼 보이지만, 반복 횟수만큼 객체를 생성해야 합니다.
반복문 내에서의 문자열 연결은, 런타임에 연결할 문자열의 개수와 내용이 결정됩니다.
이런 결우, 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복에서 문자열이 어떻게 변할지 예측할 수 없습니다.
따라서, 이런 상황에서는 최적화가 어렵습니다.
StringBuilder 는 물론이고, 아마도 대략 반복 횟수인 100,000의 String 객체를 생성했을 것입니다.
실행 결과
result = Hello Java Hello Java Hello Java...
time = 2564ms
1000ms = 1초
M1 맥북을 기준으로 100000회 더했을 때 약 2.6초가 걸렸습니다.
이럴때는 직접 StringBuilder를 사용하면 됩니다.
public class LoopStringBuilderMain {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append("Hello Java ");
}
long endTime = System.currentTimeMillis();
String result = sb.toString();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
실행 결과
result = Hello Java Hello Java Hello Java...
time = 3ms
1000ms = 1초
M1 맥북을 기준으로 100000회 더했을 때 약 0.003초가 걸렸습니다.
3️⃣ 정리
문자열을 합칠 때 대부분의 경우 최적화가 되므로 + 연산을 사용하면 됩니다.
StringBuilder를 직접 사용하는 것이 더 좋은 경우
반복문에서 반복해서 문자를 연결할 때
조건문을 통해 동적으로 문자열을 조합할 때
복잡한 문자열의 특정 부분을 변경해야 할 때
매우 긴 대용량 문자열을 다룰 때
참고: StringBuilder vs StringBuffer
StringBuilder와 똑같은 기능을 수행하는 StringBuffer 클래스도 있습니다.
StringBuffer는 내부에 동기화가 되어 있어서, 멀티 스레드 상황에 안전하지만 동기화 오버헤드로 인해 성능이 느립니다.
StringBuilder는 멀티 쓰레드 상황에 안전하지 않지만 동기화 오버헤드가 없으므로 속도가 빠릅니다.
-
💾 [CS] 프록시 패턴과 프록시 서버
💾 [CS] 프록시 패턴과 프록시 서버
1️⃣ 프록시 패턴
프록시 패턴(proxy pattern)은 대상 객체(subject)에 접근하기 전 그 접근에 대한 흐름을 가로채 해당 접근을 필터링하거나 수정하는 등의 역할을 하는 계층에 있는 디자인 패턴입니다.
이를 통해 객체의 속성, 변환 등을 보완하며 보안, 데이터 검증, 캐싱, 로깅에 사용합니다.
이는 앞서 설명한 프록시 객체로 쓰이기도 하지만 프록시 서버로도 활용됩니다.
용어: 프록시 서버에서의 캐싱
캐시 안에 정보를 담아두고, 캐시 안에 있는 정보를 요구하는 요청에 대해 다시 저 멀리 있는 원격 서버에 요청하지 않고 캐시 안에 있는 데이터를 활용하는 것을 말합니다.
이를 통해 불필요하게 외부와 연결하지 않기 때문에 트래픽을 줄일 수 있다는 장점이 있습니다.
2️⃣ 프록시 서버
프록시 서버(proxy server)는 서버와 클라이언트 사이에서 클라이언트가 자신을 통해 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해주는 컴퓨터 시스템이나 응용 프로그램을 가리킵니다.
프록시 서버로 쓰는 nginx
nginx는 비동기 이벤트 기반의 구조와 다수의 연결을 효과적으로 처리 가능한 웹 서버이며, 주로 Node.js 서버 앞단의 프록시 서버로 활용됩니다.
Node.js의 창시자 라인언 달은 다음과 같이 말했습니다 “Node.js의 버퍼 오버플로우 취약점을 예방하기 위해서는 nginx를 프록시 서버로 앞단에 놓고 Node.js를 뒤쪽에 놓는 것이 좋다.”라고 한 것입니다.
이러한 말은 Node.js 서버를 운영할 때 교과서처럼 참고되어 많은 사람이 이렇게 구축하고 있습니다.
Node.js 서버를 구축할 때 앞단에 nginx를 두는 것이죠.
이를 통해 익명 사용자가 직접적으로 서버에 접근하는 것을 차단하고, 간접적으로 한 단계를 더 거치게 만들어서 보안을 강화할 수 있습니다.
위의 그림처럼 nginx를 프록시 서버로 뒤서 실제 포트를 숨길 수 있고 정적 자원을 gzip 압축하거나, 메인 서버 앞단에서의 로깅을 할 수도 있습니다.
용어: 버퍼 오버플로우
버퍼는 보통 데이터가 저장되는 메모리 공간으로, 메모리 공간을 벗어나는 경우를 말합니다. 이때 사용되지 않아야 할 영역에 데이터가 덮어씌어져 주소, 값을 바꾸는 공격이 발생하기도 합니다.
용어: gzip 압축
LZ77과 Huffman 코딩의 조합인 DEFLATE 알고리즘을 기반으로 한 압축 기술입니다.
gzip 압축을 하면 데이터 전송량을 줄일 수 있지만, 압축을 해제했을 때 서버에서의 CPU 오버 헤드도 생각해서 gzip 압축 사용 유무를 결정해야 합니다.
프록시 서버로 쓰는 CloudFlare
CloudFlare는 전 세계적으로 분산된 서버가 있고 이를 통해 어떠한 시스템의 콘텐츠 전달을 빠르게 할 수 있는 CDN 서비스입니다.
CloudFlare는 웹 서버 앞단에 프록시 서버로 두어 DDOS 공격 방어나 HTTPS 구축에 쓰입니다.
또한, 서비스를 배포한 이후에 해외에서 무안가 의심스러운 트래픽이 많이 발생하면 이 떄문에 많은 클라우드 서비스 비용이 발생할 수도 있는데, 이때 CloudFlare가 의심스러운 트래픽인지를 먼저 판단해 CAPTCHA 등을 기반으로 이를 일정부분 막아주는 역할도 수행합니다.
위의 그림처럼 사용자, 크롤러, 공격자가 자신의 웹 사이트에 접속하게 될 텐데, 이때 CloudFlare를 통해 공격자로부터 보호할 수 있습니다.
DDOS 공격 방어
DDOS는 짧은 기간 동안 네트워크에 많은 요청을 보내 네트워크를 마비시켜 웹 사이트의 가용성을 방해하는 사이버 공격 유형입니다.
CloudFlare는 의심스러운 트래픽, 특히 사용자가 접속하는 것이 하닌 시스템을 통해 오는 트래픽을 자동으로 차단해서 DDOS 공격으로부터 보호합니다.
CloudFlare의 거대한 네트워크 용량과 캐싱 전략으로 소규모 DDOS 공격은 쉽게 막아낼 수 있으며 이러한 공격에 대한 방화벽 대시보드도 제공합니다.
HTTPS 구축
서버에서 HTTPS를 구축할 때 인증서를 기반으로 구축할 수도 있습니다.
하지만 CloudFlare를 사용하면 별도의 인증서 설치 없이 좀 더 손쉽게 HTTPS를 구축할 수 있습니다.
용어: CDN(Content Delivery Network)
각 사용자가 인터넷에서 접속하는 곳과 가까운 곳에서 콘텐츠를 캐싱 또는 배포하는 서버 네트워크를 말합니다.
이를 통해 사용자가 웹 서버로부터 콘텐츠를 다운로드하는 시간을 줄일 수 있습니다.
CORS와 프런트엔트의 프록시 서버
CORS(Cross-Origin Resource Sharing)는 서버가 웹 브라우저에서 리소스를 로드할 때 다른 오리진을 통해 로드하지 못하게 하는 HTTP 헤더 기반 메커니즘입니다.
프런트엔드 개발 시 프런트엔드 서버를 만들어서 백엔드 서버와 통신할 때 주로 CROS 에러를 마주치는데, 이를 해결하기 위해 프런트엔트에서 프록시 서버를 만들기도 합니다.
용어: 오리진
프로토콜과 호스트 이름, 포트의 조합을 말합니다.
예를 들어 https://devkobe24.com:12010/test라는 주소에서 오리진은 https://devkobe24.com:12010을 뜻합니다.
예를 들어 프런트엔드에서는 127.0.0.1:3000으로 테스팅을 하는데 백엔드 서버는 127.0.0.1:12010이라면 포트 번호가 다르기 때문에 CROS 에러가 나타납니다.
이때 프록시 서버를 둬서 프런트엔드 서버에서 요청되는 오리진을 127.0.0.1:12010으로 바꾸는 것입니다.
참고로 127.0.0.1이란 루프백(loopback) IP로, 본인 IP 서버의 IP를 뜻합니다.
localhost나 127.0.0.1을 주소창에 입력하면 DNS를 거치지 않고 바로 본인 PC 서버로 연결됩니다.
위의 그림처럼 프런트엔드 서버 앞단에 프록시 서버를 놓아 /api 요청은 user API, /api2 요청은 user API2에 요청할 수 있습니다.
자연스레 CORS 에러 해결은 물론이며 다양한 API 서버와의 통신도 매끄럽게 할 수 있는 것입니다.
-
🌐[Network] 네트워크 기초 - Summary
🌐[Network] 네트워크 기초 - Summary.
1️⃣ 네트워크 기초 용어.
“네트워크” 는 전송 매체를 매개로 데이터를 교환하는 시스템의 모음입니다.
시스템과 전송 매체의 연결 지점에 대한 규격이 “인터페이스” 입니다.
시스템이 데이터를 교환할 때는 통신 규칙인 “프로토콜” 이 필요합니다.
“인터페이스” 와 “프로토콜” 은 서로 다른 시스템을 상호 연동해 동작시키기 위함이므로 반드시 “연동 형식의 통일” 이 필요하고, 이를 “표준화” 라 합니다.
“인터넷” 은 “IP” 라는 네트워크 프로토콜이 핵심적인 역할을 하는 “네트워크 집합체” 입니다.
2️⃣ 프로토콜.
상호 연동되는 시스템이 데이터를 교환할 때 사용하는 표준화된 규칙을 “프로토콜” 이라 합니다.
일반적으로 “프로토콜” 은 “동등한 위치에 있는 시스템 사이의 규칙이라는 측면이 강조” 되어 인터페이스와 구분 됩니다.
즉, 7계층 모델에서 프로토콜은 같은 계층 사이의 관계를 다루기 때문에 각각의 계층마다 서로 다른 프로토콜이 존재합니다.
일반적으로 프로토콜은 주고받은 데이터의 형식과 그 과정에서 발생하는 일련의 절차적 순서를 규정하고 있습니다.
3️⃣ 클라이언트와 서버.
“클라이언트와 서버” 의 개념은 “인터넷 서비스를 기준” 으로 “구분” 됩니다.
그 차이가 “서비스 단위” 로 이루어지므로 “임의의 호스트가 클라이언트나 서버로 고정”되지는 않습니다.
서비스를 제공하면 서버가 되고 이 서비스를 이용하면 클라이언트가 되므로 특정 서비스를 기준으로 상대적인 관점에서 클라이언트와 서버를 사용합니다.
다양한 서비스 제공을 목적으로 하는 특화된 호스트인 경우는 호스트 자체를 서버라 부르기도 합니다.
서버는 클라이언트보다 먼저 실행 상태가 되어 클라이언트의 요청에 대기해야 합니다.
4️⃣ OSI 7계층 모델.
다수의 시스템을 서로 연결해서 통신하려면 선행적으로 전체 시스템 구조를 표준화해야 합니다.
국제 표준화 단체인 ISO에서는 OSI 7계층 모델을 제안하여, 네트워크에 연결된 시스템이 갖추어야 할 기본 구조와 기능을 정의하고 있습니다.
응용 계층
표현 계층
세션 계층
전송 계층
네트워크 계층
데이터 링크 계층
물리 계층
이런 계층적인 구조로 기능을 세분화하였습니다.
일반 사용자는 응용 계층을 통해 데이터 전송을 요청하며, 이 요청은 물리 계층까지 순차적으로 전달되어 상대 호스트에 전송됩니다.
전송된 데이터는 물리 계층에서 순차적으로 응용 계층까지 전달됩니다.
5️⃣ 인터네트워킹.
네트워크와 네트워크의 연결을 인터네트워킹이라고 합니다.
인터네트워킹 기능을 수행하는 시스템을 일반적으로 게이트웨이 라 부릅니다.
게이트웨이 는 기능에 따라 종류가 다양하지만
리피터
브리지
라우터
등등..
가장 일반적인 구분 방식입니다.
리피터는 물리 계층의 기능을 지원하며, 브리지는 리피터 기능에 데이터 링크 계층의 기능이 추가된 것으로 물리 계층에서 발생한 오류를 해결해줍니다.
라우터는 물리 계층, 데이터 링크 계층, 네트워크 계층의 기능을 지원하므로 경로 선택 기능이 존재합니다.
6️⃣ 데이터 단위.
네트워크 프로토콜을 사용해 데이터를 교환할 때는 먼저 데이터를 특정 형태로 규격화하는 작업이 필요합니다.
이와 같은 한 단위의 규격으로 묶인 전송 데이터를 데이터 단위라 하며, 계층에 상관없이 호칭할 때는 통칭하여 PDU라 부릅니다.
특별히 네트워크 계층에서는 패킷, 데이터 링크 계층에서는 프레임이라는 용어가 중요하게 사용됩니다.
7️⃣ 주소의 표현.
주소의 개념은 단순히 서로를 구분한다는 고유의 목적을 넘어 주소가 가리키는 대상의 특징을 표현할 수 있습니다.
사람들은 문자로 된 이름에 익숙하지만, 0과 1로 디지털화된 환경에서는 구분자를 숫자로 된 주소로 표현할 수밖에 없습니다.
숫자로 된 주소 표현 방식은 일반 사용자에게 불편하므로 외우기 쉬운 문자 형식의 이름을 추가로 사용합니다.
인터넷에서 일반 사용자는 문자로 된 이름을 사용하고, 인터넷 내부는 숫자로 된 주소를 사용하므로 둘 사이의 변환 기능이 필요합니다.
주소에서 구분자는 유일성, 확장성, 편리성, 정보의 함축이라는 네 가지 특징을 갖습니다.
8️⃣ IP 주소.
IP 주소는 네트워크 계층의 기능을 수행하는 IP 프로토콜이 호스트를 구분하기 위하여 사용하는 주소 체계입니다.
임의의 호스트를 인터넷에 연결하려면 반드시 IP 주소를 할당받아야 합니다.
IP 주소는 32비트의 이진 숫자로 구성되는데, 보통 8비트씩 네 부분으로 나누어 십진수로 표현합니다.
IP 주소는 유일성을 보장하기 위해서 국제 표준화 기구가 전체 주소를 관리하고 할당하기 때문에 중복 주소의 사용을 원천적으로 차단합니다.
IP 주소는 임의로 할당되는 것이 아니라, 특정 규칙에 따라 인접한 주소들을 그룹으로 묶어 관리합니다.
따라서 IP 주소는 네트워크 계층에서 경로를 선택할 때 중요한 기준이 됩니다.
9️⃣ DNS 서비스
인터넷에서 호스트와 연결하려면 해당 호스트의 IP 주소를 알아야합니다.
그런데 숫자로 된 IP 주소는 기억하기 힘들어서 의미 파악이 쉬운 문자로 된 호스트 이름을 사용하는 것이 일반적입니다.
따라서 가장 먼저 수행할 작업은 DNS라는 이름과 주소 변환 기능을 이용해서 IP 주소를 얻는 것입니다.
DNS는 주소와 이름 정보를 자동으로 유지하고 관리하는 분산 데이터베이스 시스템입니다.
호스트 주소와 이름 정보는 네임 서버라는 특정한 관리 호스트가 유지하고, 주소 변환 작업이 필요한 클라이언트는 네임 서버에 요청해서 IP 주소를 얻습니다.
1️⃣0️⃣ 다양한 주소의 종류
네트워크에서 사용하는 주소는 이를 사용하는 환경에 따라 다양합니다.
OSI 7계층 모델의 각 계층에서도 목적에 따라 여러 형탱의 주소가 사용됩니다.
MAC 주소는 계층 2의 MAC 계층에서 사용하며, 일반적으로 LAN 카드에 내장되어 있습니다.
물리 계층을 통해 데이터를 전송할 때는 MAC 주소를 이용해서 호스트를 구분합니다.
IP 주소는 네트워크 계층의 기능을 수행하는 IP 프로토콜에서 사용되며, IP 패킷이 지나가는 경로를 결정하는 라우팅의 기준이 됩니다.
포트 주소는 전송 계층에서 사용하며, 호스트에서 실행되는 프로세스를 구분해줍니다.
메일 주소는 응용 계층의 메일 시스템에서 사용자를 구분하려고 사용합니다.
-
🌐[Network] 주소 정보의 관리
🌐[Network] 주소 정보의 관리.
일반 사용자가 호스트를 지칭할 때 사용하는 호스트 이름을 도메인 이름(Domain Name)이라 하며, 인터넷에서는 www.korea.co.kr과 같은 도메인 이름을 IP 주소로 변환하는 작업이 필요합니다.
초기 인터넷에서는 아주 간단한 방법으로 호스트 이름과 IP 주소를 변환하였으나, 지금은 DNS라는 분산 데이터베이스 시스템을 사용해서 보다 체계적인 방법으로 관리하고 있습니다.
1️⃣ 호스트 파일.
호스트 이름과 IP 주소를 변환하는 간단한 방법은 특정 파일(예: UNIX 시스템의 /etc/hosts)에 호스트 이름과 IP 주소의 조합을 기록하여 관리하는 것입니다.
네트워크 응용 프로그램에서는 사용자가 입력한 호스트 이름을 이 파일에서 검색하여 일대일로 대응된 IP 주소 정보를 쉽게 얻을 수 있습니다.
호스트 파일은 한 줄에 하나의 호스트 정보가 기록되며, 일반 텍스트 문서 형식으로 보관됩니다.
즉, 아래의 그림을 예로 들면 호스트 이름이 white.korea.co.kr인 시스템의 IP 주소는 211.223.201.27입니다.
네트워크 관리자는 관리 대상이 되는 모든 호스트의 이름, 주소 정보를 주기적으로 갱신하고, 이 정보를 네트워크에 연결된 모든 호스트가 복사하도록 함으로써 정보의 일관성을 유지해야 합니다.
위 그림은 네트워크 관리자가 white.korea.co.kr 에서 호스트 정보를 갱신할 때 갱신된 정보를 다른 4개의 호스트가 복사하여 저장하는 모습을 보여줍니다.
소스트 파일을 갱신하고 복사하는 작업은 보통 시스템 관리자가 수작업으로 했었습니다.
호스트가 추가되거나 삭제되면 먼저 네트워크 관리자의 호스트에서 갱신 작업이 이루어집니다
그런데 인터넷이 처음 보급되던 시기에는 호스트 파일 갱신이 생각보다 자주 발생하지 않았기 때문에 호스트 파일을 복사하는 작업도 흔하지 않았습니다.
또한 시스템 관리자가 잦은 변경을 원하지 않아서 급하지 않은 갱신은 부분적으로 늦추기도 했습니다.
하지만 지금은 DNS 서비스가 보편적으로 사용되고 있어 이처럼 호스트 파일로 관리하는 방식은 보조적으로만 사용되고 있습니다.
2️⃣ DNS
호스트 파일로 주소와 이름 정보를 관리하는 것은 간단하지만 대부분 수동으로 작업해야 한다는 단점이 있습니다.
인터넷이 확산되면서 호스트 수가 증가할수록 네트워크 관리자가 호스트 파일을 갱신하고 복사하는 작업에 많은 시간과 노력을 들여야 합니다.
특히 지금과 같이 전 세계 컴퓨터가 연결된 네트워크 환경에서는 호스트 파일로 주소와 이름을 변환하는 작업이 사실상 불가능하다고 볼 수 있습니다.
DNS(Domain Name System)는 이러한 문제점을 해결하기 위하여 고안된 것으로, 주소와 이름 정보를 자동으로 유지하고 관리하는 분산 데이터베이스 시스템입니다.
호스트 주소와 이름 정보는 네임 서버(Name Server)라는 특정한 관리 호스트가 유지하고, 주소 변환 작업이 필요한 클라이언트 네입 서버에서 요청해서 IP 주소를 얻습니다.
네트워크가 커지면 네임 서버에 보관되는 정보의 양도 자연스럽게 많아집니다.
DNS는 하나의 집중화된 네임 서버가 전체 호스트의 정보를 관리하지 않고, 여러 네임 서버에 분산하여 관리하도록 설계되었습니다.
계층 구조로 연결된 네임 서버는 자신이 관리하는 영역에 위치한 호스트 정보만 관리하며, 정보를 상호 교환하는 협력 관계를 통해서 전체 호스트 정보를 일관성 있게 유지합니다.
3️⃣ 기타 주소
네트워크에서 사용하는 주소는 이를 사용하는 환경에 따라 다양합니다.
OSI 7계층 모델의 각 계층에서도 목적에 따라 여러 형태의 주소가 사용됩니다.
인터넷에서 일반 사용자가 접할 수 있는 대표적인 주소들.
MAC 주소
MAC 주소는 계층 2의 MAC(Medium Access Protocol) 계층에서 사용하며, 일반적으로 LAN 카드에 내장되어 있습니다.
물리 계층을 통해 데이터를 전송할 때는 MAC 주소를 이용해서 호스트를 구분합니다.
따라서 네트워크 계층이 하위의 데이터 링크 계층에 데이터 전송을 요청하면 먼저 IP 주소를 MAC 주소로 변환하는 작업이 이루어지고, 이후 MAC 계층이 상대방 MAC 계층에 데이터를 전송할 수 있습니다.
IP 주소
IP 주소는 인터넷에서 네트워크 계층의 기능을 수행하는 IP 프로토콜에서 사용되며, 송신자 IP 주소와 수신자 IP 주소로 구분됩니다.
수신자 IP 주소는 IP 패킷이 지나가는 경로를 결정하는 라우팅의 기준이 됩니다.
포트 주소
포트 주소(Port Address)는 전송 계층에서 사용하며, 호스트에서 실행되는 프로세스를 구분해줍니다.
인터넷에서 연결의 완성은 호스트와 호스트 사이가 아닌, 네트워크 응용 프로세스와 네트워크 응용 프로세스 사이입니다.
예를 들어, 내 스마트폰의 메신저 앱과 상대방 스마트폰의 메신저 앱 사이의 연결이 필요하다.
이때, 하나의 IP 주소를 갖는 스마트폰에서 실행되는 여러 네트워크 응용 앱들을 구분하는 주소가 포트 주소입니다.
인터넷의 전송 계층 프로토콜인 TCP와 UDP가 독립적으로 포트 주소를 관리하며, 포트 번호 또는 소켓 주소라는 용어를 사용하기도 합니다.
메일 주소
메일 주소는 응용 계층의 메일 시스템에서 사용자를 구분하려고 사용합니다.
kobe@korea.co.kr 처럼 사용자 이름과 호스트 이름을 @ 문자로 구분해 표기합니다.
-
-
💾 [CS] 옵저버 패턴(Observer pattern)
💾 [CS] 옵저버 패턴(Observer pattern).
옵저버 패턴(observer pattern)은 주체가 어떤 객체(subject)의 상태 변화를 관찰하다가 상태 변화가 있을 때마다 메서드 등을 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 디자인 패턴입니다.
여기서 주체란 객체의 상태 변화를 보고 있는 관찰자이며, 옵저버들이란 이 객체의 상태 변화에 따라 전달되는 메서드 등을 기반으로 ‘추가 변화 사항’이 생기는 객체들을 의미합니다.
또한, 위의 그림처럼 주체와 객체를 따로 두지 않고 상태가 변경되는 객체를 기반으로 구축하기도 합니다.
옵저버 패턴을 활용한 서비스로는 트위터가 있습니다.
위의 그림처럼 내가 어떤 사람인 주체를 ‘팔로우’ 했다면 주체가 포스팅을 올리게 되면 알림이 ‘팔로워’에게 가야합니다.
또한, 옵저버 패턴은 주로 이벤트 기반 시스템에 사용하며 MVC(Model-View-Controller) 패턴에도 사용됩니다.
예를 들어 주체라고 볼 수 있는 모델(model)에서 변경 사항이 생겨 update() 메서드로 옵저버인 뷰에 알려주고 이를 기반으로 컨트롤러(controller) 등이 작동하는 것입니다.
1️⃣ 자바에서의 옵저버 패턴.
// Observer
public interface Observer {
void update();
}
// Subject
public interface Subject {
void register(Observer obj);
void unregister(Observer obj);
void notifyObservers();
Object getUpdate(Observer obj);
}
// Topic
import java.util.ArrayList;
import java.util.List;
public class Topic implements Subject {
private List<Observer> observers;
private String message;
public Topic() {
this.observers = new ArrayList<>();
this.message = "";
}
@Override
public void register(Observer obj) {
if (!observers.contains(obj)) {
observers.add(obj);
}
}
@Override
public void unregister(Observer obj) {
observers.remove(obj);
}
@Override
public void notifyObservers() {
this.observers.forEach(Observer::update);
}
@Override
public Object getUpdate(Observer obj) {
return this.message;
}
public void postMessage(String msg) {
System.out.println("Message sended to Topic: " + msg);
this.message = msg;
notifyObservers();
}
}
// TopicSubscriber
public class TopicSubscriber implements Observer {
private String name;
private Subject topic;
public TopicSubscriber(String name, Subject topic) {
this.name = name;
this.topic = topic;
}
@Override
public void update() {
String msg = (String) topic.getUpdate(this);
System.out.println(name + ":: got message >> " + msg);
}
}
// Main
public class Main {
public static void main(String[] args) {
Topic topic = new Topic();
Observer a = new TopicSubscriber("a", topic);
Observer b = new TopicSubscriber("b", topic);
Observer c = new TopicSubscriber("c", topic);
topic.register(a);
topic.register(b);
topic.register(c);
topic.postMessage("nice to meet you");
}
}
실행 결과
Message sended to Topic: nice to meet you
a:: got message >> nice to meet you
b:: got message >> nice to meet you
c:: got message >> nice to meet you
topic을 기반으로 옵저버 패턴을 구현했습니다.
여기서 topic은 주체이자 객체가 됩니다.
class Topic implements Subject를 통해 Subject interface를 구현했고 Observer a = new TopicSubscriber("a", topic); 으로 옵저버를 선언할 때 해당 이름과 어떠한 토픽의 옵저버가 될 것인지를 정했습니다.
자바: 상속과 구현
위의 코드에 나온 implements 등 자바의 상속과 구현의 특징과 차이에 대해 알아보겠습니다.
상속(extends)
자식 클래스가 부모 클래스의 메서드 등을 상속받아 사용하며 자식 클래스에서 추가 및 확장을 할 수 있는 것을 말합니다.
이로 인해 재사용성, 중복성의 최소화가 이루어집니다.
구현(Implements)
부모 인터페이스(Interface)를 자식 클래스에서 재정의하여 구현하는 것을 말합니다.
상속과는 달리 반드시 부모 클래스의 메서드를 재정의하여 구현해야 합니다.
상속과 구현의 차이
상속은 일반 클래스, abstract 클래스를 기반으로 구현하며, 구현은 인터페이스를 기반으로 구현합니다.
-
🍃[Spring] `@Transactional` 애노테이션
🍃[Spring] @Transactional 애노테이션.
@Transactional 애노테이션은 Spring Framework에서 제공하는 애노테이션으로, 메서드나 클래스에 적용하여 해당 범위 내의 데이터베이스 작업을 하나의 트랜잭션으로 관리할 수 있도록 해줍니다.
즉, @Transactional을 사용하면 지정된 메서드 또는 클래스 내의 데이터베이스 작업이 모두 성공해야만 커밋(commit)되고, 그렇지 않으면 롤백(rollback)됩니다.
1️⃣ 주요 기능.
1. 트랜잭션 관리.
@Transactional 애노테이션이 적용된 메서드 내에서 수행되는 모든 데이터베이스 작업(예: INSERT, UPDATE, DELETE)은 하나의 트랜잭션으로 관리됩니다.
만약 메서드 실행 중 예외가 발생하면, 해당 트랜잭션 내의 모든 변경 사항이 롤백됩니다.
2. 적용 범위.
@Transactional은 클래스나 메서드에 적용할 수 있습니다.
클래스에 적용하면 해당 클래스의 모든 메서드가 트랜잭션 내에서 실행됩니다.
메스트에 적용되면 해당 메서드만 트랜잭션으로 관리됩니다.
3. 트랜잭션 전파(Propagation)
@Transactional은 여러 전파(Propagation) 옵션을 제공하여 트랜잭션이 다른 트랜잭션과 어떻게 상호작용할지를 정의할 수 있습니다.
REQUIRED : 기본값으로, 현재 트랜잭션이 존재하면 이를 사용하고, 없으면 새로운 트랜잭션을 생성합니다.
REQUIRES_NEW : 항상 새로운 트랜잭션을 생성하고, 기존 트랜잭션을 일시 정지합니다.
MANDATORY : 현재 트랜잭션이 반드시 존재해야 하며, 없으면 예외가 발생합니다.
SUPPORT : 현재 트랜잭션이 있으면 이를 사용하고, 없으면 트랜잭션 없이 실행합니다.
기타 : NOT_SUPPORT, NEVER, NESTED 등.
4. 트랜잭션 격리 수준(Isolation Level)
데이터베이스 트랜잭션의 격리 수준을 설정할 수 있습니다.
이는 동시에 실행되는 여러 트랜잭션 간의 상호작용 방식을 정의합니다.
READ_UNCOMMITTED : 다른 트랜잭션의 미완료 변경 사항을 읽을 수 있습니다.
READ_COMMITED : 다른 트랜잭션의 커밋된 변경 사항만 읽을 수 있습니다.
REPEATABLE_READ : 트랜잭션 동안 동일한 데이터를 반복적으로 읽어도 동일한 결과를 보장합니다.
SERIALIZABLE : 가장 엄격한 격리 수준으로, 트랜잭션이 완전히 순차적으로 실행되도록 보장합니다.
5. 롤백 규칙(Rollback Rules)
기본적으로 @Transactional 은 RuntimeException 또는 Error 가 발생하면 트랜잭션을 롤백합니다.
특정 예외에 대해 롤백을 강제하거나, 롤백을 방지하도록 설정할 수 있습니다.
rollbackFor 또는 noRollbackFor 속성을 사용하여 이 동작을 커스터마이징할 수 있습니다.
6. 읽기 전용(Read-Only)
@Transactional(readOnly = true)로 설정하면 트랜잭션이 데이터 읽기 전용으로 동작하며, 이 경우 데이터 수정 작업이 최적화될 수 있습니다.
주로 SELECT 쿼리에서 사용됩니다.
2️⃣ 예시 코드
@Service
public class MyService {
@Autowired
private MyRepository myRepository;
@Transactional
public void performTransaction() {
// 데이터베이스에 새로운 엔티티 추가
myRepository.save(new MyEntity("Data1"));
// 다른 데이터베이스 작업 수행
myRepository.updateEntity(1L, "UpdatedData");
// 예외 발생 시, 위의 모든 작업이 롤백됨
if (someConditionFails()) {
throw new RuntimeException("Transaction failed, rolling back...");
}
}
@Transactional(readOnly = true)
public List<MyEntity> getEntities() {
return myRepository.findAll();
}
}
3️⃣ 요약.
@Transactional 은 데이터베이스 트랜잭션을 쉽게 관리할 수 있도록 해주는 Spring의 핵심 애노테이션입니다.
이 애노테이션을 사용하면 메서드나 클래스의 데이터베이스 작업을 하나의 트랜잭션으로 처리하며, 실패 시 자동으로 롤백됩니다.
또한, 트랜잭션 전파, 격리 수준, 롤백 규칙 등을 통해 세부적으로 트랜잭션의 동작을 제어할 수 있습니다.
이러한 기능 덕분에 @Transactional은 Spring 애플리케이션에서 데이터 일관성과 무결성을 유지하는 데 중요한 역할을 합니다.
-
🍃[Spring] `@SpringBootTest` 애노테이션
🍃[Spring] @SpringBootTest 애노테이션.
@SpringBootTest 애노테이션은 Spring Boot 애플리케이션에서 통합 테스트를 수행하기 위해 사용하는 애너테이션입니다.
이 애노테이션은 테스트 클래스에서 Spring Application Context를 로드하고, 실제 애플리케이션의 전체 또는 부분적인 환경을 시뮬레이션하여 테스트할 수 있게 합니다.
주로 애플리케이션의 전반적인 기능을 테스트하거나 여러 레이어(예: 서비스, 레포지토리 등) 간의 상호작용을 검증할 때 사용됩니다.
1️⃣ 주요 기능 및 사용 방식.
1. Spring Application Context 로드
@SpringBootTest 는 기본적으로 애플리케이션의 전체 컨텍스트를 로드합니다.
이 컨텍스트는 실제 애플리케이션을 구동할 때와 동일하게 설정되어, 테스트 환경에서 애플리케이션이 어떻게 동작하는지 검증할 수 있습니다.
2. 테스트 환경 설정
@SpringBootTest 애노테이션은 다양한 속성을 통해 테스트 환경을 설정할 수 있습니다.
예를 들어, 특정 프로파일을 활성화하거나, 테스트용 설정 파일을 사용할 수 있습니다.
@SpringBootTest(properties = "spring.config.name=test-application")와 같이 속성을 지정할 수 있습니다.
3. 웹 환경 설정
@SpringBootTest는 다양한 웹 환경 모드를 지원합니다.
WebEnviroment.MOCK : 기본값으로, 웹 환경 없이 MockMvc를 사용해 서블릿 환경을 모킹합니다.
WebEnviroment.RANDOM_PORT : 테스트에 임의의 포트를 사용하여 내장 서버를 시작합니다.
WebEnviroment.DEFINED_PORT : 애플리케이션이 구성된 기본 포트를 사용합니다.
WebEnviroment.NONE : 웹 환경 없이 애플리케이션 컨텍스트만 로드합니다.
예시
@SpringBootTest(webEnvironment = SpringBootTest.WebEnviroment.RANDOM_PORT)
4. 통합 테스트
이 애노테이션은 단위 테스트와 달리, 애플리케이션의 여러 계층이 통합된 상태에서 테스트를 수행합니다.
이는 데이터베이스, 웹 서버, 서비스 레이어 등이 실제로 어떻게 상호작용하는지를 확인할 수 있게 해줍니다.
5. TestConfiguration 클래스 사용 가능
@SpringBootTest 와 함께 @TestConfiguration 을 사용하여 테스트를 위해 특별히 구성된 빈(Bean)이나 설정을 정의할 수 있습니다.
2️⃣ 예시 코드
@SpringBootTest
public class MyServiceIntegrationTest {
@Autowired
private MyService myService;
@Test
public void testServiceMethod() {
// Given
// Setup initial conditions
// When
String result = myService.performAction();
// Then
assertEquals("ExpectedResult", result);
}
}
위의 예시에서 @SpringBootTest 는 MyServiceIntegrationTest 클래스가 Spring Application Context에서 실행될 수 있도록 하고, MyService 빈이 실제로 주입되어 테스트가 실행됩니다.
3️⃣ 요약
@SpringBootTest는 Spring Boot 애플리케이션에서 통합 테스트를 쉽게 수행할 수 있도록 돕는 강력한 애노테이션입니다.
이 애노테이션을 사용하면 애플리케이션의 전체 컨텍스트를 로드한 상태에서 테스트할 수 있으며, 복잡한 애플리케이션 시나리오를 검증하는 데 유용합니다.
-
☕️[Java] JDBC(Java Database Connectivity)
☕️[Java] JDBC(Java Database Connectivity)
JDBC(Java Database Connectivity)는 자바 프로그램이 데이터베이스에 연결하고, SQL 쿼리를 실행하며, 데이터베이스로부터 결과를 가져오는 것을 가능하게 하는 자바 API입니다.
JDBC는 Java의 표준 API로, 다양한 관계형 데이터베이스 시스템(RDBMS)과의 상호 작용을 단순화하는 데 사용됩니다.
1️⃣ 주요 기능 및 개념.
1. 데이터베이스 연결
JDBC를 사용하면 자바 애플리케이션에서 데이터베이스에 연결할 수 있습니다.
이를 위해서는 데이터베이스의 URL, 사용자 이름, 비밀번호 등을 사용하여 Connection 객체를 생성합니다.
2. SQL 쿼리 실행
연결이 설정된 후, SQL 쿼리를 실행할 수 있습니다.
이는 Statement, PreparedStatment, CallableStatement 와 같은 JDBC 인터페이스를 통해 이루어집니다.
Statement는 정적 SQL 쿼리를 실행할 때 사용됩니다.
PreparedStatement는 동적 SQL 쿼리를 미리 컴파일하고, 반복 실행할 때 효율적으로 사용할 수 있습니다.
CallableStatement는 데이터베이스의 저장 프로시저를 호출할 때 사용됩니다.
3. 결과 처리
SQL 쿼리의 결과는 ResultSet 객체를 통해 얻을 수 있습니다.
ResultSet은 데이터베이스로 부터 검색된 데이터를 테이블 형식으로 제공합니다.
4. 트랜잭션 관리
JDBC는 데이터베이스 트랜젝션을 관리하는 기능도 제공합니다.
기본적으로 자동 커밋 모드이지만, 필요에 따라 수동으로 트랜잭션을 관리하고 커밋하거나 롤백할 수 있습니다.
5. 에러 처리
JDBC는 데이터베이스 관련 작업 중 발생하는 예외를 처리하기 위해 SQLException 클래스를 사용합니다.
이를 통해 에러 코드를 확인하고, 적절한 예외 처리를 할 수 있습니다.
2️⃣ JDBC의 구성 요소.
Driver
JDBC 드라이버는 특정 데이터베이스와 자바 애플리케이션 간의 통신을 담당합니다.
각 DBMS는 고유한 JDBC 드라이버를 제공합니다.
Connection
데이터베이스 연결을 표현하며, SQL 쿼리를 실행할 때 사용되는 객체를 생성합니다.
Statement
SQL 쿼리를 데이터베이스에 전달하는 역할을 합니다.
ResultSet
쿼리의 결과를 포함하며, 데이터를 읽을 수 있게 합니다.
3️⃣ JDBC의 작동 원리.
1. 드라이버 로드
애플리케이션이 사용할 JDBC 드라이버를 로드합니다.
2. 데이터베이스 연결
DriverManager.getConnection() 메서드를 사용하여 데이터베이스에 연결합니다.
3. SQL 쿼리 실행
Statement 나 PreparedStatement 객체를 사용하여 SQL 쿼리를 실행합니다.
4. 결과 처리
ResultSet 객체를 사용하여 쿼리 결과를 처리합니다.
5. 자원 해제
사용된 ResultSet, Statement, Connection 객체를 명시적으로 닫아 자원을 해제합니다.
JDBC는 데이터베이스와의 직접적인 통신을 가능하게 하며, Java 애플리케이션에서 데이터베이스 연동을 위해 널리 사용됩니다.
-
-
💾 [CS] API(Application Programming Interface)
💾 [CS] API(Application Programming Interface)
API(Application Programming Interface) 는 소프트웨어 간의 상호작용을 가능하게 해주는 인터페이스입니다.
쉽게 말해서 , API는 서로 다른 소프트웨어 시스템이나 애플리케이션이 데이터를 주고 받거나 기능을 사용할 수 있도록 도와주는 규칙과 도구들의 집합입니다.
1️⃣ API의 주요 개념.
1. 인터페이스
API는 소프트웨어 시스템이 다른 시스템이나 애플리케이션과 어떻게 소통할 수 있는지를 정의하는 인터페이스입니다.
이 인터페이스는 어떤 데이터나 기능이 노출되고, 그것들을 어떻게 사용할 수 있는지 규정합니다.
2. 추상화
API는 복잡한 시스템 내부의 구현 세부 사항을 숨기고, 사용자나 개발자가 이해하기 쉽게 필요한 기능만을 제공합니다.
예를 들어, 파일을 열거나, 데이터베이스에 쿼리를 보내거나, 웹 페이지의 데이터를 가져오는 등의 작업을 API를 통해 간단하게 수행할 수 있습니다.
3. 모듈화
API는 특정 기능이나 서비스에 대한 접근을 모듈화합니다.
이렇게 모듈화된 API를 사용하면 개발자가 시스템의 다른 부분에 영향을 주지 않고 독립적으로 기능을 사용하거나 확장할 수 있습니다.
4. 표준화
API는 표준화된 방법으로 기능에 접근할 수 있게 해주기 때문에, 여러 개발자나 시스템이 일관된 방식으로 상호작용할 수 있습니다.
예를 들어, REST API는 웹 기반 애플리케이션에서 데이터를 주고받는 표준 방식입니다.
2️⃣ API의 유형.
1. Web API(웹 API)
웹 서비스나 웹 애플리케이션에서 기능을 제공하는 API입니다.
주로 HTTP를 통해 요청과 응답을 주고받으며, REST, SOAP, GraphQL 등이 그 예입니다.
2. Library API
특정 프로그래밍 언어에서 사용할 수 있는 라이브러리나 프레임워크의 함수와 클래스들에 대한 인터페이스입니다.
예를 들어, Python의 표준 라이브러리에서 제공하는 os 나 sys 모듈도 API의 일종입니다.
3. Operating System API
운영 체제가 제공하는 기능에 접근할 수 있게 해주는 API입니다.
예를 들어, Windows API는 윈도우 애플리케이션이 운영 체제의 기능(파일 관리, UI 구성 요소, 네트워크 등)에 접근할 수 있도록 합니다.
4. Database API
데이터베이스와의 상호작용을 위해 제공되는 API입니다.
JDBC(Java Database Connectivity)는 자바 애플리케이션이 데이터베이스와 상호작용할 수 있도록 돕는 대표적인 데이터베이스 API입니다.
3️⃣ API의 예.
Google Maps API
개발자가 자신의 애플리케이션에 지도 기능을 통합할 수 있도록 Google에서 제공하는 API입니다.
Twitter API
개발자가 트위터의 기능(예: 트윗 가져오기, 트윗 작성)을 자신의 애플리케이션에 통합할 수 있도록 제공되는 API입니다.
Payment Gateway API
PayPal이나 Stripe 같은 결제 서비스에서 제공하는 API로, 애플리게이션에 결제 기능을 통합할 수 있습니다.
4️⃣ API의 중요성.
API는 소프트웨어 개발에서 매우 중요한 역할을 합니다.
그것은 소프트웨어 간의 상호 운용성을 촉진하며, 새로운 애플리케이션을 개발하거나 기존 애플리케이션에 새로운 기능을 추가하는 것을 더 쉽게 만들어 줍니다.
또한, API를 통해 외부 시스템이나 서비스와 통합할 수 있어, 다양한 기능을 제공하는 애플리케이션을 보다 효율적으로 개발할 수 있습니다.
-
🍃[Spring] 자바 코드로 직접 스프링 빈 등록하기.
🍃[Spring] 자바 코드로 직접 스프링 빈 등록하기.
스프링에서 자바 코드로 스프링 빈을 직접 등록하는 방법은 주로 @Configuration 애노테이션과 @Bean 애노테이션을 사용하여 이루어 집니다.
이 방식은 XML 설정 파일 대신 자바 클래스를 사용하여 스프링 빈을 정의하고 관리하는 방법입니다.
1️⃣ @Configuration 과 @Bean 을 사용한 빈 등록
@Configuration
이 애노테이션은 해당 클래스가 하나 이상의 @Bean 메서드를 포함하고 있으며, 스프링 컨테이너에서 빈 정의를 생성하고 처리할 수 있는 설정 클래스임을 나타냅니다.
@Bean
이 애노테이션은 메서드 레벨에서 사용되며, 메서드의 리턴값이 스프링 컨테이너에 의해 관리되는 빈(Bean)이 됨을 나타냅니다.
2️⃣ 예시.
아래는 MemoryMemberRepository 클래스를 자바 코드로 스프링 빈으로 등록하는 방법을 보여주는 예시입니다.
1. 빈으로 등록할 클래스 정의
```java
package com.devkobe.hello_spring.repository;
import com.devkobe.hello_spring.domain.Member;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
} } ```
2. 자바 설정 파일에서 빈 등록
```java
package com.devkobe.hello_spring.config;
import com.devkobe.hello_spring.repository.MemberRepository;
import com.devkobe.hello_spring.repository.MemoryMemberRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
} } ```
3. 스프링 컨테이너에서 빈 사용
```java
package com.devkobe.hello_spring.service;
import com.devkobe.hello_spring.repository.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// 비즈니스 로직 메서드들... } ```
3️⃣ 설명.
AppConfig 클래스
@Configuration 애노테이션을 사용하여 이 클래스가 스프링 설정 클래스로 사용될 것임을 명시합니다.
memberRepository() 메서드는 @Bean 애노테이션으로 정의되어 있으며, 이 메서드의 리턴값이 스프링 컨테이너에 의해 관리되는 빈이 됩니다.
이 경우, MemoryMemberRepository 객체가 빈으로 등록됩니다.
빈 사용
MemberService 클래스에서 MemberRepository 타입의 빈이 생성자 주입 방식으로 주입됩니다.
이때, AppConfig 클래스에서 등록된 MemoryMemberRepository 빈이 주입됩니다.
4️⃣ 왜 자바 설정을 사용할까?
1. 타입 안정성
자바 코드는 컴파일 시점에 타입을 체크할 수 있으므로, XML보다 타입 안정성이 높습니다.
2. IDE 지원
자바 기반 설정은 IDE의 자동 완성 기능과 리팩토링 도구를 잘 지원받을 수 있습니다.
3. 코드 재사용성
자바 설정 클래스는 일반 자바 코드처럼 재사용 사능하며, 상속과 조합 등을 활용할 수 있습니다.
5️⃣ 결론
스프링에서 자바 코드로 빈을 등록하는 방법은 @Configuration 과 @Bean 애노테이션을 사용한 방법입니다.
이 방식은 XML 기반 설정보다 더 타입 안전하고, 유지보수하기 쉬우며, 현대적인 스프링 애플리케이션에서 자주 사용됩니다.
-
🌐[Network] 주소와 이름
🌐[Network] 주소와 이름.
시스템을 지칭하는 구분자는 내부에서 처리되는 숫자 기반의 주소(Address)와 함께 사용자의 이해와 편리성을 도모하는 문자로 된 이름(Name)을 제공해야 합니다.
일반 사용자는 내부 주소를 몰라도 이름만으로 시스템에 접근할 수 있어야 하며, 이름과 주소를 연결하는 방법은 시스템 내부적으로 처리되어야 합니다.
네트워크의 규모가 크지 않아서 관리하는 시스템의 개수가 적은 경우에는 간단한 형식의 주소와 이름을 사용할 수 있으므로 이를 관리하는 시스템도 크게 복잡하지 않습니다.
그러나 관리 대상이 많아지면 주소와 이름의 공간이 커지고, 이를 관리하는 시스템의 기능도 복잡해집니다.
네트워크에는 여러 종류의 주소와 이름이 존재합니다.
이는 각 계층의 기능을 담당하는 프로토콜마다 주소를 독립적으로 관리하기 때문입니다.
예를 들어, IP 프로토콜은 호스트를 구분하기 위하여 IP 주소를 사용하며, 데이터 링크 계층에서는 LAN 카드별로 MAC 주소를 따로 부여합니다.
전송 계층을 수행하는 TCP에서는 호스트에서 수행되는 네트워크 프로세스마다 별도의 포트(Port) 주소를 할당하고 관리합니다.
1️⃣ IP 주소
IP 주소(IP Address)는 네트워크 계층의 기능을 수행하는 IP 프로토콜이 호스트를 구분하기 위하여 사용하는 주소 체계입니다.
임의의 호스트를 인터넷에 연결하려면 반드시 IP 주소를 할당 받아야 합니다.
IP 주소는 32비트의 이진 숫자로 구성되는데, 보통 8비트씩 네 부분으로 나누어 십진수로 표현합니다.
위 그림은 32비트의 이진수 11010011 11011111 11001001 00011110은 인터넷에서 사용하는 실제 IP 주소입니다.
일반 사용자는 이진수에 익숙하지 않고, 그 길이도 길어서 외우기 쉽지 않습니다.
따라서 이를 4개의 십진수로 변환한 후 각각을 점(.)으로 구분한 211.223.201.30으로 표기합니다.
이와 같은 숫자로 된 주소조차 외우기 어려우므로 문자로된 www.korea.co.kr 등의 도메인 이름을 사용합니다.
IP 주소는 유일성을 보장하기 위해서 국제 표준화 기구가 전체 주소를 관리하고 할당하기 때문에 중복 주소의 사용을 원천적으로 차단합니다.
IP 프로토콜이 처음 개발될 당시에는 현재처럼 폭넓게 활용되리라 예측하지 못했습니다.
따라서 IP 주소로 표현할 수 있는 최대 주소 공간의 크기를 32비트로 제한함으로써 확장성에 많은 문제점이 야기되고 있습니다.
이를 해결하기 위하여 새로운 프로토콜인 IPv6(Internet Protocol Version 6)에서는 주소 표현 공간을 128비트로 확장했습니다.
그리고 현재의 IP 프로토콜은 IPv6과 구분하기 위해 IPv4로 표현합니다.
IP 주소는 임의로 할당되는 것이 아니라, 특정 규칙에 따라 인접한 주소들을 그룹으로 묶어 관리합니다.
따라서 IP 주소는 네트워크 계층에서 경로를 선택할 때 중요한 기준이 됩니다.
위 그림에서 네트워크 1에는 IP 주소가 211.223.201로 시작하는 호스트들이 있고, 네트워크 2에는 211.223.202로 시작하는 호스트들이 있습니다.
왼쪽의 인터넷에서 임의의 호스트가 보낸 패킷이 중간의 라우터에 도착한 경우 이 패킷의 목적지 주소가 211.223.201.30 이라면 당연히 네트워크 1로 중개해야 합니다.
이처럼 인터넷에서 IP 주소는 패킷의 경로를 결정하는 데 중요한 역할을 합니다.
그림에 설명된 원리에 의하여, 인터넷에서 네트워크 계층 기능을 수행하는 IP 프로토콜이 전송 패킷의 경로를 결정합니다.
2️⃣ 호스트 이름
인터넷에서 특정 호스트와 연결하려면 반드시 해당 호스트의 IP 주소를 알아야 하고, 인터넷 내부의 네트워크 계층은 호스트를 IP 주소로 구분합니다.
그런데 일반 사용자는 숫자로 된 IP 주소를 기억하기 힘듭니다.
그래서 사용자들은 의미 파악이 쉬운 문자로 된 호스트 이름을 사용하는 것이 일반적입니다.
위 그림은 일반 사용자가 문자로 된 호스트 이름을 사용하였을 때 IP 주소로 변환되는 과정을 보여줍니다.
맨 밑에 있는 네트워크 계층의 IP 프로토콜은 호스트를 구분하는 용도로 IP 주소만 사용합니다.
그에 비해 일반 사용자는 IP 주소보다는 문자로 된 호스트 이름을 사용하기 때문에 중간 계층에서 이를 변환하는 기능을 수행해야 합니다.
일반적으로 FTP, 텔넷과 같은 네트워크 응용 프로그램은 실행 과정에서 사용자로부터 호스트 이름을 명령어 인수로 입력받습니다.
따라서 가장 먼저 수행할 작업은 DNS(Domain Name System)라는 이름과 주소 변환 기능을 이용해서 IP 주소를 얻는 것입니다.
이후 변환된 IP 주소의 호스트에 연결 설정이나 전송 데이터가 포함된 패킷을 전송합니다.
DNS 서비스는 호스트 이름을 , , , 라는 네 계층 구조로 나누고, 이들을 점(.)으로 구분해서 표기합니다.
예를 들어, www.korea.co.kr과 같은 호스트 이름은 대한민국(kr)에 있는 일반 회사(co) 중에서 korea라는 이름의 회사에 소속된 www라는 호스트를 의미합니다.
<호스트>.<단체 이름>.<단체 종류>.<국가 도메인>
은 가 위치한 국가의 이름을 두 글자의 약자로 표시합니다.
아래의 표처럼 나라마다 고유한 <국가 도메인이 존재합니다.
는 기관의 성격에 따라 부여하며, 사용 예는 아래의 표와 같습니다.
은 보통 단체를 상징하는 이름을 사용합니다.
예를 들어, 회사는 회사명을, 학교는 학교 이름을 사용합니다.
마지막으로 는 소속 단체의 네트워크 관리자가 내부 규칙에 따라 개별 호스테에 부여한 이름을 사용합니다.
-
☕️[Java] String 클래스 - 기본
☕️[Java] String 클래스 - 기본
1️⃣ String 클래스 - 기본
자바에서 문자를 다루는 대표적인 타입은 char, String 2가지가 있습니다.
public class CharArrayMain {
public static void main(String[] args) {
char[] charArr = new char[]{'h', 'e', 'l', 'l', 'o'};
System.out.println(charArr);
String str = "hello";
System.out.println("str = " + str);
}
}
실행 결과
hello
str = hello
기본형은 char 는 문자 하나를 다룰 때 사용합니다.
char 를 사용해서 여러 문자를 나열하려면 char[] 을 사용해야합니다.
하지만 이렇게 char[] 을 직접 다루는 방법은 매우 불편하기 때문에 자바는 문자열을 매우 편리하게 다룰 수 있는 String 클래스를 제공합니다.
String 클래스를 통해 문자열을 생성하는 방법은 2가지가 있습니다.
public class StringBasicMain {
public static void main(String[] args) {
String str1 = "hello";
String str2 = new String("hello");
System.out.println("str1 = " + str1);
System.out.println("str2 = " + str2);
}
}
쌍따옴표 사용 : "hello"
객체 생성 : new String("hello");
String 은 클래스입니다.
int, boolean 같은 기본형이 아니라 참조형입니다.
따라서 str1 변수에는 String 인스턴스의 참조값만 들어갈 수 있습니다.
그러므로 다음 코드는 뭔가 어색합니다.
String str1 = "hello";
문자열은 매우 자주 사용됩니다.
그래서 편의상 쌍따옴표로 문자열을 감싸면 자바언어 에서 new String("hello") 와 같이 변경해줍니다.
이 경우 실제로는 성능 최적화를 위해 문자열 풀을 사용합니다.
String str1 = "hello"; // 기존
String str1 = new String("hello"); // 변경
2️⃣ String 클래스 구조
String 클래스는 대략 다음과 같이 생겼습니다.
public final class String {
// 문자열 보관
private final char[] value; // 자바 9 이전
private final byte[] value; // 자바 9 이후
// 여러 메서드
public String concat(String str) {...}
public int length() {...}
}
클래스이므로 속성과 기능을 가집니다.
속성(필드)
private final char[] value;
여기에는 String 의 실제 문자열 값이 보관됩니다.
문자 데이터 자체는 char[] 에 보관됩니다.
String 클래스는 개발자가 직접 다루기 불편한 char[] 을 내부에 감추고 String 클래스를 사용하는 개발자가 편리하게 문자열을 다룰 수 있도록 다양한 기능을 제공합니다.
그리고 메서드 제공을 넘어서 자바 언어 차원에서도 여러 편의 문법을 제공합니다.
참고: 자바 9 이후 String 클래스 변경 사항
자바 9부터 String 클래스에서 char[] 대신에 byte[]을 사용합니다.
private final byte[] value;
자바에서 문자 하나를 표현하는 char는 2byte를 차지합니다.
그런데 영어, 숫자는 보통 1byte로 표현이 가능합니다.
그래서 단순 영어, 숫자로만 표현된 경우 1byte를 사용하고(정확히는 Latin-1 인코딩의 경우 1byte 사용)
그렇지 않은 나머지의 경우 2byte인 UTF-16 인코딩을 사용합니다.
따라서 메모리를 더 효율적으로 사용할 수 있게 변경되었습니다.
기능(메서드)
String 클래스는 문자열로 처리할 수 있는 다양한 기능을 제공합니다.
기능이 방대하므로 필요한 기능이 있으면 검색하거나 API 문서를 찾아보면 됩니다.
주요 메서드는 다음과 같습니다.
length() : 문자열의 길이를 반환합니다.
charAt(int index) : 특정 인덱스의 문자를 반환합니다.
substring(int beginIndex, int endIndex) : 문자열의 부분 문자열을 반환합니다.
indexOf(String str) : 특정 문자열이 시작되는 인덱스를 반환합니다.
toLowerCase(), toUpperCase() : 문자열을 소문자 또는 대문자로 변환합니다.
trim() : 문자열 양 끝의 공백을 제거합니다.
concat(String str) : 문자열을 더합니다.
3️⃣ String 클래스와 참조형.
String 은 클래스입니다.
따라서 기본형이 아니라 참조형입니다.
참조형은 변수에 계산할 수 있는 값이 들어오는 것이 아니라 x001 과 같이 계산할 수 없는 참조값이 들어있습니다.
따라서 원칙적으로 + 같은 연산을 사용할 수 없습니다.
public class StringConcatMain {
public static void main(String[] args) {
String a = "hello";
String b = " java";
String result1 = a.concat(b);
String result2 = a + b;
System.out.println("result1 = " + result1);
System.out.println("result2 = " + result2);
}
}
자바에서 문자열을 더할 때는 String 이 제공하는 concat() 과 같은 메서드를 사용해야 합니다.
하지만 문자열은 너무 자주 다루어지기 때문에 자바 언어에서 편의상 특별히 + 연산을 제공합니다.
실행 결과
result1 = hello java
result2 = hello java
-
💾 [CS] 전략 패턴(Strategy pattern)
💾 [CS] 전략 패턴(Strategy pattern)
1️⃣ 전략 패턴(Strategy pattern)
전략 패턴(Strategy pattern) 은 정책 패턴(Policy pattern) 이라고도 하며, 객채의 행위를 바꾸고 싶은 경우 ‘직접’ 수정하지 않고 전략이라고 부르는 ‘캡슐화한 알고리즘’ 을 컨텍스트 안에서 바꿔주면서 상호 교체가 가능하게 만드는 패턴입니다.
아래의 예시 코드는 우리가 어떤 것을 살 때 네이버페이, 카카오페이 등 다양한 방법으로 결제하듯이 어떤 아이템을 살 때 LUNACard로 사는 것과 KAKAOCard로 사는 것을 구현한 예제입니다.
결제 방식의 ‘전략’ 만 바꿔서 두 가지 방식으로 결제하는 것을 구현했습니다.
// PaymentStrategy - interface
public interface PaymentStrategy {
void pay(int amount);
}
// KAKAOCardStrategy
public class KAKAOCardStrategy implements PaymentStrategy{
private String name;
private String cardNumber;
private String cvv;
private String dateOfExpiry;
public KAKAOCardStrategy(String name, String cardNumber, String cvv, String dateOfExpiry) {
this.name = name;
this.cardNumber = cardNumber;
this.cvv = cvv;
this.dateOfExpiry = dateOfExpiry;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid using KAKAOCard.");
}
}
// LUNACardStrategy
public class LUNACardStrategy implements PaymentStrategy {
private String emailId;
private String password;
public LUNACardStrategy(String emailId, String password) {
this.emailId = emailId;
this.password = password;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid using LUNACard");
}
}
// Item
public class Item {
private String name;
private int price;
public Item(String name, int price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
}
// ShoppingCart
import java.util.ArrayList;
import java.util.List;
public class ShoppingCart {
List<Item> items;
public ShoppingCart() {
this.items = new ArrayList<>();
}
public void addItem(Item item) {
this.items.add(item);
}
public void removeItem(Item item) {
this.items.remove(item);
}
public int calculateTotal() {
int sum = 0;
for (Item item : items) {
sum += item.getPrice();
}
return sum;
}
public void pay(PaymentStrategy pamentMethod) {
int amount = calculateTotal();
pamentMethod.pay(amount);
}
}
// Main
import designPattern.strategy.Item;
import designPattern.strategy.KAKAOCardStrategy;
import designPattern.strategy.LUNACardStrategy;
import designPattern.strategy.ShoppingCart;
public class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
Item A = new Item("A", 100);
Item B = new Item("B", 300);
cart.addItem(A);
cart.addItem(B);
// pay by LUNACard
cart.pay(new LUNACardStrategy("kobe@google.com", "1234"));
// pay by KAKAOCard
cart.pay(new KAKAOCardStrategy("Minseong Kang", "123456789", "123", "12/01"));
}
}
실행 결과
400 paid using LUNACard
400 paid using KAKAOCard.
위 코드는 쇼핑 카드에 아이템을 담아 LUNACard 또는 KAKAOCard 라는 두 개의 전략으로 결제하는 코드입니다.
용어 : 컨텍스트
프로그래밍에서의 컨텍스트는 상황, 맥락, 문맥을 의미하며 개발자가 어떠한 작업을 완료하는 데 필요한 모든 관련 정보를 말합니다.
-
🍃[Spring] 스프링 컨테이너(Spring Container)란?
🍃[Spring] 스프링 컨테이너(Spring Container)란?
1️⃣ 스프링 컨테이너(Spring Container)란?
스프링 컨테이너(Spring Container)는 스프일 프레임워크의 핵심 구성 요소로, 애플리케이션에서 사용되는 객체들은 관리하고 조정하는 역할을 합니다.
이 컨테이너는 객체의 생성, 초기화, 의존성 주입, 설정 및 라이프사이클을 관리하여 애플리케이션의 주요 컴포넌트들이 잘 협력할 수 있도록 돕습니다.
스프링 컨테이너는 종종 IoC(Inversion of Control) 컨테이너 또는 DI(Dependency Injection) 컨테이너 라고도 불립니다.
2️⃣ 스프링 컨테이너의 주요 기능.
1. 빈(Bean) 관리
스프링 컨테이너는 애플리케이션에 필요한 모든 빈(Bean)을 정의하고 생성합니다.
이 빈들은 XML 설정 파일, 자바 설정 클래스, 또는 애노테이션을 통해 정의될 수 있습니다.
빈의 라이프사이클(생성, 초기화, 소멸)을 관리하고, 의존성을 자동으로 주입하여 빈 간의 결합도를 낮추어 줍니다.
2. 의존성 주입(Dependency Injection)
스프링 컨테이너는 객체 간의 의존성을 자동으로 주입하여, 객체들이 직접 다른 객체를 생성하거나 관리하지 않도록 합니다.
이를 통해 코드의 유연성과 재사용성을 높입니다.
의존성 주입은 생성자 주입, 세터 주입, 필드 주입 등 다양한 방법으로 이루어질 수 있습니다.
3. 설정 관리
컨테이너는 애플리케이션의 설정 정보를 관리합니다.
이는 빈의 정의뿐만 아니라, 데이터베이스 연결 설정, 메시지 소스, 트랜잭션 관리 등의 다양한 설정을 포함합니다.
4. 라이프사이클 인터페이스 지원
컨테이너는 빈의 라이프사이클 인터페이스(InitializingBean, DisposableBean)을 통해 빈의 초기화 및 소명 작업을 쉽게 구현할 수 있도록 지원합니다.
또한 @PostConstruct, @PreDestroy 애노테이션을 통해 라이프사이클 콜백을 간단하게 구현할 수 있습니다.
5. AOP(Aspect-Oriented Programming) 지원
스프링 컨테이너는 AOP 기능을 지원하여, 애플리케이션 전반에 걸쳐 공통적으로 사용되는 로직(예: 로깅, 트랜잭션 관리)을 비즈니스 로직과 분리하여 모듈화할 수 있게 합니다.
3️⃣ 스프링 컨테이너의 종류.
스프링에는 다양한 컨테이너 구현체가 있으며, 대표적으로 다음과 같은 종류가 있습니다.
1. BeanFactory
스프링의 가장 기본적인 컨테이너로, 빈의 기본적인 생성과 의존성 주입을 제공합니다.
하지만 BeanFactory는 지연 로딩(lazy loading) 방식으로 동작하므로, 빈이 실제로 요청될 때 생성됩니다.
2. ApplicationContext
BeanFactory의 확장판으로, 대부분의 스프링 애플리케이션에서 사용되는 컨테이너입니다.
ApplicationContext 는 BeanFactory의 기능을 포함하면서도, 다양한 기능(예: 이벤트 발행, 국제화 메시지 처리, 환경 정보 관리)을 추가로 제공합니다.
ApplicationContext 의 구현체에는 ClassPathXmlApplicationContext, FileSystemXmlApplicationContext, AnnotationConfigApplicationContext 등이 있습니다.
4️⃣ 스프링 컨테이너의 동작 과정.
1. 빈 정의 로드
컨테이너가 시작되면, XML 파일, 자바 설정 파일, 애노테이션 등을 통해 빈의 정의를 읽어들입니다.
2. 빈 생성 및 초기화
컨테이너는 필요한 빈들을 생성하고 초기화 작업을 수행합니다.
이때 의존성이 있는 경우, 필요한 빈들을 먼저 생성하여 주입합니다.
3. 의존성 주입
빈의 생성 과정에서 필요한 의존성들이 주입됩니다.
이 과정에서 생성자 주입, 세터 주입 등이 사용됩니다.
4. 빈 제공
컨테이너는 요청 시 빈을 제공하며, 애플리케이션은 이 빈을 통해 다양한 작업을 수행할 수 있습니다.
5. 빈 소멸
애플리케이션이 종료되거나 컨테이너가 종료될 때, 컨테이너는 빈의 소멸 작업을 처리합니다.
스프링 컨테이너는 이 모든 과정을 자동으로 처리하며, 이를 통해 개발자는 비즈니스 로직에 집중할 수 있게됩니다.
-
🍃[Spring] 계층형 아키텍처(Layered Architecture), 3계층 아키텍처(Three-Tier Architecture)
🍃[Spring] 계층형 아키텍처(Layered Architecture), 3계층 아키텍처(Three-Tier Architecture)
Controller를 통해 외부의 요청을 받고, Service에서 비즈니스 로직을 처리하며, Repository에서 데이터를 저장하고 관리하는 패턴은 “계층형 아키텍처(Layered Architecture)” 또는 “3계층 아키텍처(Three-Tier Architecture)” 라고 부릅니다.
1️⃣ 계층형 아키텍처(Layered Architecture)
이 아키텍처 패턴은 애플리케이션을 여러 계층으로 나누어 각 계층이 특정한 역할을 담당하도록 구조와합니다.
이 방식은 소프트웨어의 복잡성을 줄이고, 코드의 유지보수성을 높이며, 테스트하기 쉽게 만들어줍니다.
스프링 프레임워크에서 이 패턴은 자주 사용됩니다.
2️⃣ 각 계층별 역할.
1. Presentation Layer(프레젠테이션 계층) - Controller
사용자 인터페이스와 상호작용하는 계층입니다.
외부의 요청을 받아서 처리하고, 응답을 반환합니다.
스프링에서는 주로 @Controller 또는 @RestController 를 사용하여 이 계층을 구현합니다.
2. Business Logic Layer(비즈니스 로직 계층) - Service
비즈니스 로직을 처리하는 계층입니다.
데이터의 처리, 계산, 검증 등 핵심적인 애플리케이션 로직이 구현됩니다.
스프링에서는 주로 @Service 애노테이션을 사용하여 이 계층을 구현합니다.
3. Data Access Layer(데이터 접근 계층) - Repository
데이터베이스나 외부 데이터 소스와 상호작용하는 계층입니다.
데이터의 CRUD(Create, Read, Update, Delete) 작업을 처리합니다.
스프링에서는 주로 @Repository 애노테이션을 사용하여 이 계층을 구현하며 JPA, MyBatis, Hibernate 등의 ORM(Object-Relational Mapping) 도구와 함께 사용됩니다.
3️⃣ 이 패턴의 주요 장점.
모듈화
각 계층이 독립적으로 관리되므로, 각 계층의 코드가 명확히 분리됩니다.
유지보수성
비즈니스 로직, 데이터 접근, 그리고 프레젠테이션 로직이 분리되어 있어, 각 부분을 독립적으로 수정하거나 확장하기 쉽습니다.
테스트 용이성
각 계층을 독립적으로 테스트할 수 있어, 단위 테스트와 통합 테스트가 용이합니다.
유연성
특정 계층을 변경하거나 대체할 때, 다른 계층에 미치는 영향을 최소화할 수 있습니다.
이 계층형 아키텍처는 스프링 프레임워크를 사용하는 대부분의 애플리케이션에서 채택하는 일반적인 구조이며, 소프트웨어 설계의 베스트 프랙티스 중 하나로 널리 인정받고 있습니다.
-
🍃[Spring] 의존성 주입(Dependency Injection)을 통한 느슨한 결합(Loose Coupling) 유지.
🍃[Spring] 의존성 주입(Dependency Injection)을 통한 느슨한 결합(Loose Coupling) 유지.
아래의 코드에서 @Autowired 로 MemberService 클래스의 생성자에 MemberRepository 인터페이스를 주입받는 이유는 의존성 주입(Dependency Injection) 을 통해 느슨한 결합(Loose Coupling) 을 유지하기 위합입니다.
이는 객체지향 설계에서 매우 중요한 원칙 중 하나로, 클래스 간의 결합도를 낮춰 코드의 유연성과 확장성을 높이는 데 기여합니다.
1️⃣ 전체 코드.
// MemberRepository
import com.devkobe.hello_spring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
// MemoryMemberRepository
import com.devkobe.hello_spring.domain.Member;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.stereotype.Repository;
@Repository
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
}
}
// MemberService
import com.devkobe.hello_spring.domain.Member;
import com.devkobe.hello_spring.repository.MemberRepository;
import com.devkobe.hello_spring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
/*
* 회원 가입
*/
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
/*
* 전체 회원 조회
*/
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
2️⃣ 구체적인 이유.
1. 인터페이스를 통한 유연성 확보.
MemberRepository 는 인터페이스로, MemoryMemberRepository 와 같은 구현체들이 이 인터페이스를 구현합니다.
인터페이스를 통해 MemberService 는 특정 구현체에 의존하지 않으며, MemberRepository 인터페이스에 의존하게 됩니다.
이는 MemberService 가 MemoryMemberRepository 나 다른 MemberRepository 구현체(JdbcMemberRepository, JpaMemberRepository 등)에 쉽게 교체될 수 있음을 의미합니다.
예를 들어, 나중에 메모리가 아닌 데이터베이스에 회원 정보를 저장하는 방식으로 전환하고 싶다면, MemoryMemberRepository 대신 새로운 구현체를 주입하면 됩니다.
2. 느슨한 결합.
MemberService 는 MemberRepository 인터페이스에만 의존하기 때문에, 어떤 구현체가 실제로 사용될지는 스프링 컨테이너가 결정합니다.
이렇게 하면 MemberService 는 구현체가 무엇인지 알 필요가 없으므로, 구현체가 변경되더라도 MemberService 를 수정할 필요가 없습니다.
이 방식은 유지보수성을 크게 향상시킵니다.
새로운 저장소 방식이 도입되더라도, 기존 비즈니스 로직에 영향을 주지 않고 새로운 기능을 추가할 수 있습니다.
3. 스프링의 의존성 주입 메커니즘 활용.
스프링은 자동으로 @Autowired 를 사용하여 적절한 MemberRepository 구현체를 찾아 주입합니다.
MemoryMemberRepository 클래스에 @Repository 애노테이션이 붙어 있기 때문에, 스프링 컨테이너는 이 클래스를 MemberRepository 타입의 빈으로 인식하고 관리하게 됩니다.
스프링은 MemberRepository 인터페이스 타입의 빈을 주입해야 하는 경우, 해당 인터페이스를 구현한 클래스 중 하나를 선택해 주입합니다.
이 예제에서는 MemoryMemberRepository 가 주입됩니다.
3️⃣ 결론.
이러한 설계는 코드의 유연성과 테스트 용이성을 크게 향상시킵니다.
인터페이스를 사용함으로써, MemberService 는 특정 구현체에 구애받지 않고 다양한 환경에서 재사용될 수 있습니다.
또한, 이는 스프링의 DI 원칙에 따라, 컴포넌트 간의 결합도를 낮추고, 애플리케이션이 변화에 잘 대응할 수 있도록 설계하는 방법입니다.
-
💾 [CS] 도메인(Domain)의 의미.
💾 [CS] 도메인(Domain)의 의미.
도메인(Domain) 은 소프트웨어 개발에서 특정 문제 영역 또는 비즈니스 영역을 지칭하는 용어입니다.
도메인은 소프트웨어 시스템이 해결하고자 하는 문제나 제공하는 서비스와 관련된 특정한 지식, 규칙, 절차 등을 포함한 모든 것을 의미합니다.
1️⃣ 도메인(Domain)의 의미.
1. 문제 영역
도메인은 특정 비즈니스나 문제 영역을 나타내며, 이 영역은 소프트웨어가 해결하려고 하는 실제 세계의 문제와 직접적으로 관련됩니다.
예를 들어, 은행 업무, 전자상거래, 병원 관리, 교육 관리 시스템 등 각각의 도메인은 서로 다른 문제와 규칙을 가지고 있습니다.
2. 도메인 지식
도메인에는 해당 문제 영역에 대한 전문 지식이나 규칙이 포함됩니다.
예를 들어, 금융 도메인에서는 이자 계산, 대출 규정, 계좌 관리와 같은 특정 지식이 중요합니다.
이와 같은 도메인 지식을 바탕으로 소프트웨어의 비즈니스 로직이 정의됩니다.
3. 도메인 모델
도메인은 일반적으로 “도메인 모델(Domain Model)”로 표현됩니다.
도메인 모델은 도메인의 개념, 객체, 엔티티, 관계, 규칙 등을 추상화하여 표현한 것입니다.
예를 들어, 은행 도메인 모델에는 고객(Customer), 계좌(Account), 거래(Transaction) 같은 객체가 포함될 수 있습니다.
도메인 모델은 시스템이 해당 도메인의 문제를 어떻게 해결할지를 정의하는데 중요한 역할을 합니다.
4. 도메인 전문가
도메인 전문가(Domain Expert)는 특정 도메인에 대한 깊은 지식을 가진 사람을 의미합니다.
이들은 비즈니스 핵심 요구 사항과 규칙을 정의하며, 개발자와 협력하여 도메인 모델을 설계하는데 중요한 역할을 합니다.
2️⃣ 도메인의 중요성
도메인은 소프트웨어 개발의 초기 단계에서 매우 중요합니다.
시스템이 해결해야 하는 문제를 명확히 정의하고, 비즈니스 요구 사항을 반영한 도메인 모델을 설계하는 것이 시스템의 성공적인 구현에 필수적입니다.
도메인 지식을 제대로 반영하지 못하면, 시스템이 실제 비즈니스 문제를 해결하는 데 실패할 수 있으며, 이는 프로젝트 실패로 이어질 수 있습니다.
따라서 개발자는 도메인 전문가와 긴밀하게 협력하여 도메인을 정확히 이해하고, 이를 코드로 표현하는 것이 중요합니다.
3️⃣ 도메인 주도 설계(Domain-Driven Design, DDD)
도메인과 관련된 중요한 소프트웨어 설계 접근법 중 하나는 도메인 주도 설계(Domain-Driven Design, DDD) 입니다.
DDD는 도메인 모델을 중심으로 소프트웨어를 설계하는 방법론으로, 도메인의 개념과 규칙을 코드에 직접 반영하여 소프트웨어의 복잡성을 관리하고, 도메인의 변화에 쉽게 적응할 수 있도록 돕습니다.
예시
예를 들어, 전자상거래 도메인 을 생각해보면, 이 도메인에는 다음과 같은 요소들이 포함될 수 있습니다.
고객(Customer) : 상품을 구매하는 사람.
상품(Product) : 고객이 구매할 수 있는 아이템.
주문(Order) : 고객이 상품을 구매할 때 생성되는 거래 기록.
결제(Payment) : 주문에 대한 대금 지불.
이러한 요소들과 그들 간의 관계가 도메인을 구성하며, 소프트웨어 시스템은 이러한 도메인의 개념을 바탕으로 비즈니스 로직을 구현하게 됩니다.
4️⃣ 결론
도메인은 소프트웨어가 다루는 문제의 범위와 관련된 개념, 규칙, 객체들을 나타내며, 이를 정확히 이해하고 모델링하는 것이 성공적인 소프트웨어 개발의 핵심입니다.
도메인 이해를 바탕으로 적절한 비즈니스 로직을 구현하는 것이 소프트웨어의 목표를 달성하는 데 매우 중요합니다.
-
💾 [CS] 비즈니스 로직(Business Logic)이란?
💾 [CS] 비즈니스 로직(Business Logic)이란?
1️⃣ 비즈니스 로직(Business Logic).
비즈니스 로직(Business Logic) 은 소프트웨어 시스템 내에서 특정 비즈니스 도메인에 대한 규칙, 계산, 절차 등을 구현한 부분을 의미합니다.
이 로직은 애플리케이션이 실제 비즈니스 요구 사항을 충족하도록 하는 핵심 기능을 담당합니다.
비즈니스 로직(Business Logic) 은 시스템이 처리해야 하는 업무 규칙과 관련된 의사결정을 포함하며, 데이터의 유효성 검사를 하고, 비즈니스 프로세스를 관리하고, 관련된 계산을 수행하는 역할을 합니다.
2️⃣ 비즈니스 로직의 주요 역할
1. 도메인 규칙 관리.
특정 비즈니스 도메인에서 따라야 하는 규칙을 정의하고 관리합니다.
예를 들어, 은행 시스템에서 계좌 이체 시 잔액이 충분해야 한다는 규칙을 비즈니스 로직에서 처리합니다.
2. 유효성 검사.
입력된 데이터나 시스템 내부에서 사용되는 데이터가 비즈니스 규칙에 맞는지 검증합니다.
예를 들어, 사용자가 입력한 주문의 총액이 0보다 큰지, 재고가 충분한지 등을 검사하는 로직이 포함됩니다.
3. 비즈니스 프로세스 구현.
비즈니스 워크플로우를 구현하여, 각 단계에서 수행해야 하는 작업을 정의하고, 순서대로 실행되도록 관리합니다.
예를 들어, 주문 처리 시스템에서 주문 접수, 결제 처리, 배송 준비 등의 단계가 비즈니스 로직에 포함될 수 있습니다.
4. 계산과 처리.
특정 비즈니스 규칙에 따라 데이터를 계산하거나 처리하는 역할을 합니다.
예를 들어, 세금 계산, 할인 적용, 이자 계산 등이 여기에 포함됩니다.
3️⃣ 비즈니스 로직의 위치
비즈니스 로직은 보통 애플리케이션의 Service 계층 에 위치합니다.
이 계층은 데이터를 처리하는 로직과 사용자 인터페이스를 담당하는 로직을 분리하여, 코드의 재사용성을 높이고 유지보수를 용이하게 합니다.
Service 계층 에서는 비즈니스 로직을 구현하며, 필요한 경우 데이터 접근 계층(Repository) 을 호출하여 데이터를 조회하거나 저장하고, 최종적으로 처리된 결과를 프레젠테이션 계층(Controller) 에 전달합니다.
4️⃣ 비즈니스 로직과 다른 로직의 구분
비즈니스 로직
실제 비즈니스와 관련된 모든 규칙과 프로세스를 정의합니다.
이는 특정 도메인 지식에 기반하며, 도메인 전문가가 주로 요구 사항을 정의합니다.
프레젠테이션 로직
사용자 인터페이스와 관련된 로직으로, 사용자에게 데이터를 표시하거나 입력을 받는 것과 관련됩니다.
데이터 접근 로직
데이터베이스와 상호작용하며, 데이터를 저장하거나 조회하는 작업을 담당합니다.
5️⃣ 비즈니스 로직의 중요성
비즈니스 로직은 애플리케이션의 핵심적인 부분이므로, 이 로직의 정확성은 시스템 전체의 신뢰성과 직결됩니다.
잘 설계된 비즈니스 로직은 애플리케이션이 요구된 비즈니스 목표를 정확히 달성할 수 있도록 돕고, 변경이 필요할 때도 쉽게 확장하거나 수정할 수 있도록합니다.
따라서 비즈니스 로직을 구현할 때는 도메인 전문가와 긴밀하게 협력하여 요구사항을 명확히 이해하고, 이를 코드로 정확히 표현하는 것이 매우 중요합니다.
-
🍃[Spring] `@Controller` 애너테이션 사용시 일어나는 일.
🍃[Spring] @Controller 애너테이션 사용시 일어나는 일.
1️⃣ 스프링 프레임워크에서 @Controller 애노테이션 사용시 어떤 일이 일어날까요?
스프링 프레임워크에서 @Controller 애노테이션을 사용하면, 해당 클래스가 스프링 MVC의 웹 컨트롤러로 동작하도록 설정됩니다.
@Controller 는 기본적으로 웹 요청을 처리하고, 적절한 응답을 생성하는 역할을 담당하는 클래스를 정의할 때 사용됩니다.
@Controller 애노테이션을 사용하면 다음과 같은 일들이 벌어집니다.
1. 스프링 빈으로 등록.
@Controller 애노테이션이 적용된 클래스는 스프링의 컴포넌트 스캔 메커니즘에 의해 자동으로 스프링 컨텍스트에 빈으로 등록됩니다.
이는 @Component 와 유사하게 동작하며, 스프링이 이 클래스를 관리하도록 만듭니다.
2. 요청 처리 메서드 매핑.
@Controller 가 달린 클래스 내의 메서드들은 @RequestMapping, @GetMapping, @PostMapping 등과 같은 요청 매핑 애노테이션을 통해 특정 HTTP 요청을 처리하는 메서드로 매핑될 수 있습니다.
이러한 매핑을 통해 특정 URL로 들어오는 요청이 어떤 메서드에 의해 처리될지 결정됩니다.
3. 모델과 뷰.
@Controller 는 주로 모델과 뷰를 처리합니다.
요청이 컨트롤러에 도달하면, 컨트롤러는 필요한 데이터를 모델에 담고, 적절한 뷰(예: JSP, Thymeleaf 템플릿)를 반환하여 사용자에게 응답을 보냅니다.
스프링은 이 작업을 쉽게 할 수 있도록 다양한 기능을 제공합니다.
4. 비즈니스 로직과 서비스 계층.
컨트롤러는 보통 직접 비즈니스 로직을 처리하지 않고, 서비스 계층을 호출하여 필요한 처리를 위임합니다.
컨트롤러는 사용자 입력을 받아 서비스로 전달하고, 서비스의 결과를 받아 사용자에게 반환하는 역할을 합니다.
5. 예외 처리.
@Controller 애노테이션을 사용하는 클래스는 또한 @ExceptionHandler 를 사용하여 특정 예외를 처리할 수 있습니다.
이를 통해 컨트롤러 내에서 발생하는 예외를 잡아 특정 응답을 반환하거나 에러 페이지를 보여줄 수 있습니다.
요약하면, @Controller 애노테이션은 해당 클래스를 스프링 MVC에서 요청을 처리하는 컨트롤러로 정의하며, HTTP 요청을 처리하고 적절한 응답을 생성하는데 중요한 역할을 합니다.
-
🍃[Spring] 빈(Bean)이란?
🍃[Spring] 빈(Bean)이란?
1️⃣ 빈(Bean)이란?
스프링 프레임워크에서 빈(Bean) 이란, 스프링 IoC(Inversion of Control) 컨테이너에 의해 관리되는 객체를 의미합니다.
스프링 빈은 애플리케이션 전반에서 사용될 수 있도록 스프링 컨텍스트에 등록된 인스턴스입니다.
빈은 보통 애플리케이션의 핵심 로직이나 비즈니스 로직을 수행하는 객체들로, 스프링은 이러한 빈들을 효율적으로 관리하고 주입합니다.
빈의 정의와 동작은 스프링의 핵심 개념인 의존성 주입(Dependency Injection, DI) 과 밀접한 관련이 있습니다.
2️⃣ 스프링 빈의 주요 특징.
1. 싱글톤(Singleton) 스코프
기본적으로 스프링 빈은 싱글톤 스코프로 관리됩니다.
즉, 특정 빈 타입에 대해 스프링 컨테이너는 하나의 인스턴스만을 생성하고 애플리케이션 내에서 재사용합니다.
물론, 필요에 따라 프로토타입, 요청, 세션 등 다른 스코프로 빈을 정의할 수도 있습니다.
2. 의존성 관리
스프링 컨테이너는 빈의 의존성을 자동으로 주입합니다.
즉, 빈이 생성될 때 필요한 의존성(다른 빈이나 리소스)을 스프링이 자동으로 주입해줍니다.
이 과정에서 생성자 주입, 세터 주입, 필드 주입 등 다양한 방법이 사용될 수 있습니다.
3. 라이프사이클 관리
스프링은 빈의 생성부터 소멸까지의 라이프사이클을 관리합니다.
빈이 생성될 때 초기화 작업을 하거나, 빈이 소멸될 때 클린업 작업을 수행할 수 있도록 다양한 훅(Hook)을 제공하며, 이 과정에서 @PostConstruct, @PreDestroy 같은 애노테이션을 사용할 수 있습니다.
4. 설정 및 구성
빈은 XML 설정 파일이나 자바 설정 클래스에서 정의될 수 있습니다.
또한, @Component, @Service, @PostConstruct, @PreDestroy 같은 애노테이션을 사용할 수 있습니다.
5. 느슨한 결합(Loose Coupling)
스프링 빈을 사용하면 객체 간의 의존성을 직접 설정하는 것이 아니라, 스프링이 관리하므로 코드가 더욱 유연하고 테스트하기 쉬워집니다.
이는 애플리케이션의 유지보수성과 확장성을 높여줍니다.
3️⃣ 스프링 빈의 정의 예시.
다음은 빈이 어떻게 정의되고, 스프링 컨테이너가 이를 관리하는지에 대한 간단한 예시입니다.
@Component
public class MyService {
public void performService() {
System.out.println("Service is being performed.")
}
}
위 코드에서 @Component 애노테이션이 적용된 MyService 클래스는 스프링 빈으로 등록됩니다.
스프링 컨테이너는 이 빈을 관리하고, 필요할 때 의존성을 주입합니다.
빈을 스프링 컨텍스트에서 가져와 사용하는 예시는 다음과 같습니다.
```java
@Autowired
private MyService myService;
public void useService() {
myService.performService();
}
```
여기서 @Autowired 애노테이션은 MyService 타입의 빈을 스프링 컨테이너에서 주입받아 useService 메서드에서 사용할 수 있도록 합니다.
결론적으로, 스프링 빈은 스프링 애플리케이션에서 핵심적인 역할을 하는 객체로, 스프링 컨테이너가 관리하는 인스턴스이며, 이를 통해 애플리케이션의 구성 요소들이 유연하고 효율적으로 동작하도록 돕습니다.
-
-
-
☕️[Java] 불변 객체 - 문제와 풀이
☕️[Java] 불변 객체 - 문제와 풀이
1️⃣ 불변 객체 - 문제와 풀이.
문제 설명
MyDate 클래스는 불변이 아니어서 공유 참조시 사이드 이펙트가 발생합니다. 이를 불변 클래스로 만들어야 합니다.
새로운 불변 클래스는 ImmutableMyDate
새로운 실행 클래스는 ImmutableMyDateMain
1️⃣ 불변이 아닌 MyDate 클래스
// MyDate
public class MyDate {
private int year;
private int month;
private int day;
public MyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
public void setYear(int year) {
this.year = year;
}
public void setMonth(int month) {
this.month = month;
}
public void setDay(int day) {
this.day = day;
}
@Override
public String toString() {
return year + "-" + month + "-" + day;
}
}
// MyDateMain
public class MyDateMain {
public static void main(String[] args) {
MyDate date1 = new MyDate(2024, 9, 1);
MyDate date2 = date1;
System.out.println("date1 = " + date1);
System.out.println("date2 = " + date2);
System.out.println("2025 -> date1");
date1.setYear(2025);
System.out.println("date1 = " + date1);
System.out.println("date2 = " + date2);
}
}
실행 결과
date1 = 2024-9-1
date2 = 2024-9-1
2025 -> date1
date1 = 2025-9-1
date2 = 2025-9-1
2️⃣ 불변 클래스인 ImmutableMyDate
// ImmutableMyDate
public class ImmutableMyDate {
private final int year;
private final int month;
private final int day;
public ImmutableMyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
public ImmutableMyDate withYear(int newYear) {
return new ImmutableMyDate(newYear, month, day);
}
public ImmutableMyDate withMonth(int newMonth) {
return new ImmutableMyDate(year, newMonth, day);
}
public ImmutableMyDate withDay(int newDay) {
return new ImmutableMyDate(year, month, newDay);
}
@Override
public String toString() {
return year + "-" + month + "-" + day;
}
}
// ImmutableMyDateMain
public class ImmutableMyDateMain {
public static void main(String[] args) {
ImmutableMyDate immutableDate1 = new ImmutableMyDate(2024, 9, 1);
ImmutableMyDate immutableDate2 = immutableDate1;
System.out.println("immutableDate1 = " + immutableDate1);
System.out.println("immutableDate2 = " + immutableDate2);
System.out.println("2025 -> immutableDate1");
// 방법 1.
//immutableDate1 = new ImmutableMyDate(2025, 9, 1);
// 방법 2. -> 이 방법을 더 지향함
// 주의: 불변 객체에서 값을 변경하는 메서드가 있을 경우에는 무조건 반환값을 받아서 참조를 가지고 가야 바뀐 값을 사용할 수 있습니다.
immutableDate1 = immutableDate1.withYear(2025); // x002
System.out.println("immutableDate1 = " + immutableDate1); // x002
System.out.println("immutableDate2 = " + immutableDate2); // x001
}
}
실행 결과
immutableDate1 = 2024-9-1
immutableDate2 = 2024-9-1
2025 -> immutableDate1
immutableDate1 = 2025-9-1
immutableDate2 = 2024-9-1
3️⃣ 참고 - withXxx()
불변 객체에서 값을 변경하는 경우 withYear() 처럼 “with” 로 시작하는 경우가 많습니다.
예를 들어, “coffee with sugar” 라고 하면, 커피에 설탕이 추가되어 원래의 상태를 변경하여 새로운 변형을 만든가는 것을 의미합니다.
이 개념을 프로그래밍에 적용하면, 불변 객체의 메서드가 “with” 로 이름 지어진 경우, 그 메서드가 지정된 수정사항을 포함하는 객체의 새 인스턴스를 반환한다는 사실을 뜻합니다.
정리하면 “with” 는 관례처럼 사용되는데, 원본 객체의 상태가 그대로 유지됨을 강조하면서 변경사항을 새 복사본에 포함하는 과정을 간결하게 표현합니다.
-
🌐[Network] 주소의 표현
🌐[Network] 주소의 표현.
시스템을 설계할 때는 기능이나 목적과 함께 고유의 구분자(Identifier)를 부여하는 방법에 대해서도 우선하여 고려해야 합니다.
일반적으로 주소의 개념은 단순히 서로를 구분한다는 고유 목적을 넘어서 주소가 가리키는 대상의 특징을 표현할 수 있습니다.
사람들은 문자로 된 이름에 익숙하지만, 0과 1로 디지털화된 환경에서는 구분자를 숫자로 된 주소로 표현할 수밖에 없습니다.
디지털 환경에서 숫자로 된 주소 표현 방식은 일반 사용자에게 불편하므로 보통은 외우기 쉬운 문자 형식의 이름을 추가로 사용합니다.
주소와 이름은 일대일(1:1) 관계가 이루어지며, 이들은 연결하는 기능이 필요합니다.
인터넷에서 일반 사용자는 문자로 된 이름을 사용하고, 인터넷 내부는 숫자로 된 주소를 사용하므로 둘 사이의 변환 기능이 필요합니다.
대상을 유일하게 구별하는 구분자는 일반적으로 다음의 네 가지 특징이 있습니다.
1️⃣ 유일성
구분자의 가장 중요한 역할은 대상을 서로 구분하여 지칭하는 것입니다.
따라서 서로 다른 대상이 같은 구분자를 갖지 않는 유일성을 보장해야 합니다.
그러나 이론적으로 완전한 확장성을 전제로 하는 유일성을 보장하기는 불가능합니다.
예를 들어, 주민 번호에서 앞쪽 여섯 글자인 생년월일은 100년 이내에 출생한 사람들만 구분할 수 있습니다.
현재는 방편적으로 바로 뒤의 남녀 구분 자리(1900년대 출생자는 1,2를 사용하고, 2000년대 출생자는 3,4를 사용함)를 활용하여 제한적인 확장성을 확보하고 있을 뿐 입니다.
2️⃣ 확장성
시스템은 시간이 흐르면서 이용자가 증가하는 보편화 과정이 진행되므로 자연스럽게 규모가 확장됩니다.
따라서 사용하는 구분자의 양도 증가합니다.
시스템의 최대 수용 규모를 예측하여 구분자의 최대 한계를 올바르게 설정하지 않으면, 표현할 수 있는 공간의 크기가 제한되어 시스템의 확장성도 제한받게 됩니다.
합리적인 기준을 설정하여 확장의 정도를 예측하고, 또한 그 이후에 대한 고려도 함께 이루어져야 합니다.
처음 인터넷을 설계했을 때 지금과 같은 규모로 인터넷을 이용하리라고는 예측하지 못했습니다.
그 결과 인터넷 구분자인 IP 주소의 고갈 문제에 직면해 있습니다.
3️⃣ 편리성
시스템 설계 과정에서 부여되는 구분자는 시스템의 내부 처리 구조를 효율적으로 운용할 수 있도록 해주어야 합니다.
컴퓨터 시스템은 내부적으로 숫자에 기반해 처리되기 때문에 구분자의 체계도 숫자 위주입니다.
또한 배치, 검색 등을 원활하게 수행하기 위해 보통 일반인이 의미를 이해할 수 없는 형식을 갖습니다.
이처럼 시스템 내부 동작에 종속된 구분자의 주소 체계는 사용자가 쉽게 이해하기 어려우므로 문자로 된 이름을 추가로 부여합니다.
따라서 숫자로 된 주소와 문자로 된 이름을 모두 가지므로 이를 매핑(Mapping)하는 기능이 필요합니다.
4️⃣ 정보의 함축
구분자는 응용 환경에 필요한 다양한 정보를 포함하는 경우가 많습니다.
예를 들어, 주민 번호는 생년월일, 성별 등을 알 수 있는 숫자로 구성되어 있습니다.
집 주소도 광역시부터 시작해 지역을 소규모로 분할하는 구조로 되어 있어 집의 지리적인 위치를 쉽게 가늠할 수 있습니다.
이처럼 분자는 응용 환경에 적절히 대응할 수 있는 부가 정보를 포함해야 합니다.
-
☕️[Java] 불변 객체 - 예제
☕️[Java] 불변 객체 - 예제
조금 더 복잡하고 의미있는 예제를 통해서 불변 객체의 사용 예를 확인해봅시다.
1️⃣ Address, ImmutableAddress 코드.
// Address
public class Address {
private String value;
public Address(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return "Address{" +
"value='" + value + '\'' +
'}';
}
}
// ImmutableAddress
public class ImmutableAddress {
private final String value;
public ImmutableAddress(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return "Address{" +
"value='" + value + '\'' +
'}';
}
}
2️⃣ 변경 클래스 사용.
// MemberV1
public class MemberV1 {
private String name;
private Address address;
public MemberV1(String name, Address address) {
this.name = name;
this.address = address;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
@Override
public String toString() {
return "MemberV1{" +
"name='" + name + '\'' +
", address=" + address +
'}';
}
}
// MemberMainV1
public class MemberMainV1 {
public static void main(String[] args) {
Address address = new Address("서울");
MemberV1 memberA = new MemberV1("회원A", address);
MemberV1 memberB = new MemberV1("회원B", address);
// 회원A, 회원B의 처음 주소는 모두 서울
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
// 회원B의 주소를 부산으로 변경해야함
memberB.getAddress().setValue("부산");
System.out.println("부산 -> memberB.address");
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
}
}
회원A 와 회원B 는 둘 다 서울에 살고 있습니다.
중간에 회원B 의 주소를 부산으로 변경해야 합니다.
그런데 회원A 와 회원B 는 Address 인스턴스를 참조하고 있습니다.
회원B 의 주소를 부산으로 변경하는 순간 회원A 의 주소도 부산으로 변경됩니다.
실행 결과
memberA = MemberV1{name='회원A', address=Address{value='서울'}}
memberB = MemberV1{name='회원B', address=Address{value='서울'}}
부산 -> memberB.address
memberA = MemberV1{name='회원A', address=Address{value='부산'}}
memberB = MemberV1{name='회원B', address=Address{value='부산'}}
3️⃣ 불변 클래스 사용.
// MemberV2
public class MemberV2 {
private String name;
private ImmutableAddress address;
public MemberV2(String name, ImmutableAddress address) {
this.name = name;
this.address = address;
}
public ImmutableAddress getAddress() {
return address;
}
public void setAddress(ImmutableAddress address) {
this.address = address;
}
@Override
public String toString() {
return "MemberV1{" +
"name='" + name + '\'' +
", address=" + address +
'}';
}
}
MemberV2 는 주소를 변경할 수 없는, 불변인 ImmutableAddress 를 사용합니다.
// MemberMainV2
public class MemberMainV2 {
public static void main(String[] args) {
ImmutableAddress address = new ImmutableAddress("서울");
MemberV2 memberA = new MemberV2("회원A", address);
MemberV2 memberB = new MemberV2("회원B", address);
// 회원A, 회원B의 처음 주소는 모두 서울
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
// 회원B의 주소를 부산으로 변경해야함
// memberB.getAddress().setValue("부산"); // 컴파일 오류
memberB.setAddress(new ImmutableAddress("부산"));
System.out.println("부산 -> memberB.address");
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
}
}
회원B 의 주소를 중간에 부산으로 변경하려고 시도합니다.
하지만 ImmutableAddress 에는 값을 변경할 수 있는 메서드가 없습니다.
따라서 컴파일 오류가 발생합니다.
결국 memberB.setAddress(new ImmutableAddress("부산")) 와 같이 새로운 주소 객체를 만들어서 전달합니다.
실행 결과
memberA = MemberV1{name='회원A', address=Address{value='서울'}}
memberB = MemberV1{name='회원B', address=Address{value='서울'}}
부산 -> memberB.address
memberA = MemberV1{name='회원A', address=Address{value='서울'}}
memberB = MemberV1{name='회원B', address=Address{value='부산'}}
사이드 이펙트가 발생하지 않습니다. 회원A 는 기존 주소를 그대로 유지합니다.
-
💾 [CS] 팩토리 패턴(factory pattern)
💾 [CS] 팩토리 패턴(factory pattern).
1️⃣ 팩토리 패턴(factory pattern).
팩토리 패턴(factory pattern)은 객체를 사용하는 코드에서 객체 생성 부분을 떼어내 추상화한 패턴이자 상속 관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대를 결정하고, 하위 클래스에서 객체 생성에 관한 구체적인 내용을 결정하는 패턴입니다.
상위 클래스와 하위 클래스가 분리되기 때문에 느슨한 결합을 가지며 상위 클래스에서는 인스턴스 생성 방식에 대해 전혀 알 필요가 없기 때문에 더 많은 유연성을 갖게 됩니다.
그리고 객체 생성 로직이 따로 떼어져 있기 때문에 코드를 리팩터링하더라도 한 곳만 고칠 수 있게 되니 유지 보수성이 증가됩니다.
예를 들어 라떼 레시피와 아메리카노 레시피, 우유 레시피라는 구체적인 내용이 들어 있는 하위 클래스가 컨베이어 벨트를 통해 전달되고, 상위 클래스인 바리스타 공장에서 이 레시피들을 토대로 우유 등을 생산하는 생산 공정을 생각하면 됩니다.
2️⃣ 자바의 팩토리 패턴
enum CoffeeType {
LATTE,
ESPRESSO
}
abstract class Coffee {
protected String name;
public String getName() {
return name;
}
}
class Latte extends Coffee {
public Latte() {
name = "latte";
}
}
class Espresso extends Coffee {
public Espresso() {
name = "Espresso";
}
}
class CoffeeFactory {
public static Coffee createCoffee(CoffeeType type) {
switch (type) {
case LATTE:
return new Latte();
case ESPRESSO:
return new Espresso();
default:
throw new IllegalArgumentException("Invalid coffee type: " + type);
}
}
}
public class Main {
public static void main(String[] args) {
Coffee coffee = CoffeeFactory.createCoffee(CoffeeType.LATTE);
System.out.println(coffee.getName()); // latte
}
}
3️⃣ 코드 설명.
팩토리 패턴(Factory Pattern) 은 객체 생성의 로직을 별도의 클래스나 메서드로 분리하여 관리하는 디자인 패턴입니다.
이는 객체 생성에 관련된 코드를 클라이언트 코드에서 분리하여, 객체 생성의 변화에 대한 유연성을 높이고 코드의 유지보수성을 개선하는 데 도움이 됩니다.
팩토리 패턴 은 크게 팩토리 메서드 패턴 과 추상 팩토리 패턴 으로 구분되며, 위 코드 예시는 팩토리 메스드 패턴 의 전형적인 예입니다.
1. CoffeeType 열거형(Enum)
enum CoffeeType {
LATTE,
ESPRESSO
}
설명 : CoffeeType 은 커피의 종류를 나타내는 열거형(Enum)입니다.
이 열거형은 LATTE 와 ESPRESSO 두 가지 타입의 커피를 정의하고 있습니다.
역할 : 커피의 종류를 코드 내에서 명확하게 구분하고, CoffeeFactory 에서 커피 객체를 생성할 때 사용됩니다.
2. Coffee 추상 클래스.
abstract class Coffee {
protected String name;
public String getName() {
return name;
}
}
설명 : Coffee 는 커피 객체의 공통된 속성과 메서드를 정의한 추상 클래스입니다.
name 필드는 커피의 이름을 저장하며, getName() 메서드는 커피의 이름을 반환합니다.
역할 : 구체적인 커피 클래스들이 상속받아야 하는 공통적인 기능을 정의합니다.
3. Latte 와 Espresso 클래스.
class Latte extends Coffee {
public Latte() {
name = "latte";
}
}
class Espresso extends Coffee {
public Espresso() {
name = "Espresso";
}
}
설명 : Latte 와 Espresso 는 Coffee 클래스를 상속받아 구체적인 커피 타입을 구현한 클래스들입니다.
각 클래스는 생성자에서 name 필드를 특정 커피 이름으로 초기화합니다.
역할 : 특정 커피 타입의 객체를 생성하는 역할을 합니다.
4. CoffeeFactory 클래스.
class CoffeeFactory {
public static Coffee createCoffee(CoffeeType type) {
switch (type) {
case LATTE:
return new Latte();
case ESPRESSO:
return new Espresso();
default:
throw new IllegalArgumentException("Invalid coffee tyep: " + type);
}
}
}
설명 : CoffeeFactory 클래스는 팩토리 패턴의 핵심으로, createCoffee() 메서드를 통해 특정 타입의 커피 객체를 생성하여 반환합니다.
CoffeeType 열거형에 따라 적절한 커피 객체를 생성합니다.
역할 : 객체 생성의 로직을 중앙 집중화하여 클라이언트 코드에서 객체 생성의 책임을 분리합니다.
클라이언트는 CoffeeFactory 의 createCoffee() 메서드를 호출하여 원하는 커피 객체를 생성할 수 있습니다.
5. Main 클래스.
public class Main {
public static void main(String[] args) {
Coffee coffee = CoffeeFactory.createCoffee(CoffeeType.LATTE);
System.out.println(coffee.getName()); // latte
}
}
설명 : Main 클래스는 클라이언트 코드로, CoffeeFactory 를 사용하여 LATTE 타입의 커피 객체를 생성하고, 그 이름을 출력합니다.
역할 : 팩토리 패턴을 사용하는 클라이언트 코드로, 직접적으로 객체를 생성하지 않고 팩토리를 통해 객체를 생성합니다.
4️⃣ 팩토리 패턴의 장점.
1. 코드의 유연성 증가.
객체 생성 로직이 중앙화되어 있으므로, 새로운 커피 타입을 추가할 때 클라이언트 코드를 수정할 필요 없이 팩토리 클래스만 수정하면 됩니다.
2. 유지보수성 향상.
객체 생성 코드가 한 곳에 모여 있어 코드의 유지보수가 쉬워집니다.
객체 생성 과정에서의 변경이 필요한 경우에도 팩토리 클래스만 수정하면 됩니다.
3. 코드의 결합도 감소.
클라이언트 코드는 구체적인 클래스에 의존하지 않고, 인터페이스나 추상 클래스를 통해 객체를 다루기 때문에 결합도가 낮아집니다.
5️⃣ 팩토리 패턴의 단점.
1. 클래스의 복잡성 증가.
객체 생성을 위한 팩토리 클래스가 추가됨으로써 클래스의 수가 증가하고, 코드 구조가 다소 복잡해질 수 있습니다.
2. 확장 시 주의 필요.
새로운 커피 타입을 추가할 때마다 팩토리 클래스의 switch 문이나 if-else 문이 증가할 수 있어, 확장성이 제한될 수 있습니다.
이 문제를 해결하기 위해서는 추상 팩토리 패턴이나 다른 디자인 패턴과 결합하는 방법을 고려할 수 있습니다.
6️⃣ 결론.
팩토리 패턴은 객체 생성의 책임을 분리하여 코드의 유연성과 유지보수성을 높이는 강력한 디자인 패턴입니다.
위 코드 예시에서는 커피 객체를 생성하는 로직을 CoffeeFactory 클래스에 모아두어, 클라이언트 코드가 특정 커피 클래스에 직접적으로 의존하지 않도록 하였습니다.
이를 통해 클라이언트 코드는 커피 객체의 생성 방식에 대해 신경 쓰지 않고도 다양한 타입의 커피를 생성하고 사용할 수 있게 됩니다.
-
💾 [CS] 추상화(Abstraction)
💾 [CS] 추상화(Abstraction).
1️⃣ 추상화(Abstraction).
추상화(Abstraction) 는 객체 지향 프로그래밍(Object-Oriented-Programming, OOP)의 중요한 개념 중 하나로, 복잡한 시스템에서 핵심적인 개념이나 기능만을 추려내어 단순화하는 과정입니다.
이를 통해 불필요한 세부 사항을 감추고, 중요한 속성이나 행위만을 노출하여 시스템을 보다 간단하게 이해하고 사용할 수 있게 합니다.
1️⃣ 추상화의 핵심 개념.
1. 본질적인 것만 노출.
시스템의 복잡한 내부 구현을 숨기고, 외부에서는 중요한 기능이나 속성만을 사용할 수 있도록 설계합니다.
예를 들어, 자동차를 운전할 때 운전자는 엔진의 작동 원리나 내부 구조를 몰라도, 운전대, 가속 페달, 브레이크 등의 중요한 인터페이스를 통해 자동차를 조작할 수 있습니다.
2. 복잡성 감소.
추상화를 통해 사용자에게 복잡한 시스템을 단순하게 보이도록 하여, 사용자가 시스템을 쉽게 이해하고 사용할 수 있게 합니다.
이는 특히 큰 시스템이나 라이브러리를 설계할 때 중요합니다.
3. 재사용성과 유지보수성 향상.
추상화를 사용하면, 코드의 재사용성을 높이고 유지보수성을 향상시킬 수 있습니다.
동일한 추상 인터페이스를 구현하는 여러 클래스가 있을 때, 구체적인 클래스 구현을 신경 쓰지 않고 인터페이스를 통해 일관된 방식으로 코드를 사용할 수 있습니다.
2️⃣ 추상화의 예
추상화는 주로 추상 클래스와 인터페이스 를 통해 구현됩니다.
추상 클래스.
추상 클래스는 하나 이상의 추상 메서드를 초함하는 클래스입니다.
추상 메서드는 선언만 되어 있고, 구체적인 구현은 해당 클래스를 상속받는 하위 클래스에서 제공해야 합니다.
abstract class Animal {
abstract void sound(); // 추상 메서드
void breathe() { // 구체적인 메서드
System.out.println("Breathing");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Woof");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("Meow");
}
}
이 예에서 Animal 클래스는 추상 클래스이고, sound() 메서드는 추상 메서드입니다.
Dog 와 Cat 클래스는 sound() 메서드를 구체적으로 구현합니다.
Animal 클래스는 동물의 일반적인 특징인 breath() 메서드를 포함하지만, sound() 는 동물마다 다르므로 하위 클래스에서 구체화됩니다.
인터페이스
인터페이스는 추상화의 또 다른 형태로, 클래스가 구현해야 하는 메서드의 선언을 포함합니다.
인터페이스 자체는 구현을 가지지 않으며, 구현은 이를 구현하는 클래스에서 제공됩니다.
interface Flyable {
void fly(); // 추상 메서드
}
class Bird implements Flyable {
@Override
public void fly() {
System.out.println("Bird is flying");
}
}
class Airplane implements Flyable {
@Override
public void fly() {
System.out.println("Airplane is flying");
}
}
여기서 Flyable 인터페이스는 fly() 라는 추상 메서드를 선언하고 있으며, Bird 와 Airplane 클래스는 각각 이 메서드를 구현합니다.
Flyable 인터페이스를 통해, 비행할 수 있는 객체들은 동일한 방식으로 취급될 수 있습니다.
3️⃣ 추상화의 장점.
1. 코드의 간결성.
중요한 부분만 남기고 복잡한 구현 세부 사항을 숨겨, 코드를 간결하고 이해하기 쉽게 만듭니다.
2. 유연한 설계.
구체적인 구현에 의존하지 않기 때문에, 다양한 구현체를 쉽게 교체하거나 확장할 수 있습니다.
3. 재사용성 증가.
추상 클래스나 인터페이스를 통해 여러 클래스에서 공통적으로 사용될 수 있는 구조를 만들 수 있습니다.
4️⃣ 요약.
추상화는 복잡한 시스템에서 불필요한 세부 사항을 감추고 중요한 부분만을 노출하여 시스템을 간단하게 만드는 개념입니다.
이를 통해 코드의 복잡성을 줄이고, 유연성과 재사용성을 높이며, 유지보수를 용이하게 할 수 있습니다.
추상화는 주로 추상 클래스와 인터페이스를 통해 구현됩니다.
-
☕️[Java] 테스트 코드와 Reflection.
☕️[Java] 테스트 코드와 Reflection.
1️⃣ 전체 코드.
// Member
public class Member {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
// MemberRepository - Interface
import com.devkobe.hello_spring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
// MemoryMemberRepository
import com.devkobe.hello_spring.domain.Member;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
}
}
// MemberService
import com.devkobe.hello_spring.domain.Member;
import com.devkobe.hello_spring.repository.MemberRepository;
import com.devkobe.hello_spring.repository.MemoryMemberRepository;
import java.util.List;
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
/*
* 회원 가입
*/
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
/*
* 전체 회원 조회
*/
public List<Member> findMembers() {
return memberRepository.findAll();
}
}
2️⃣ MemberService를 Test.
import com.devkobe.hello_spring.domain.Member;
import com.devkobe.hello_spring.repository.MemoryMemberRepository;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void setUp() throws Exception {
memberService = new MemberService();
memberRepository = new MemoryMemberRepository();
// Reflection을 사용하여 memberRepository 필드에 값을 설정.
Field repositoryField = MemberService.class.getDeclaredField("memberRepository");
repositoryField.setAccessible(true);
repositoryField.set(memberService, memberRepository);
}
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
@Test
public void 회원가입() {
// given
Member member = new Member();
member.setName("spring");
// when
Long savedId = memberService.join(member);
// then
Optional<Member> foundMember = memberRepository.findById(savedId);
assertThat(foundMember.isPresent()).isTrue();
assertThat(foundMember.get().getName()).isEqualTo("spring");
}
@Test
public void 중복_회원_제외() {
// given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
// join
memberService.join(member1);
// then
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
memberService.join(member2);
});
assertThat(exception.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
@Test
public void 전체회원조회() {
// given
Member member1 = new Member();
member1.setName("spring1");
Member member2 = new Member();
member2.setName("spring2");
memberService.join(member1);
memberService.join(member2);
// when
List<Member> members = memberService.findMembers();
// then
assertThat(members.size()).isEqualTo(2);
assertThat(members).contains(member1, member2);
}
}
3️⃣ 모르는 코드 설명.
"@BeforeEach" 애노테이션.
JUnit 5에서 테스트 메서드가 실행되기 전에 매번 호출되는 메서드에 사용됩니다.
이 애노테이션이 붙은 메서드는 각 테스트 메서드가 실행되기 직전에 실행되므로, 테스트 환경을 초기화하거나 준비 작업을 수행하는 데 유용합니다.
🙋♂️ 이 애노테이션의 주요 역할은 다음과 같습니다.
테스트 환경 초기화
각 테스트 메서드가 실행될 때마다 동일한 초기 상태를 보장하기 위해 사용됩니다.
예를 들어, 테스트할 객체를 새로 생성하거나, 필요한 데이터를 설정하는 등의 작업을 수행합니다.
반복 작업 처리
여러 테스트에서 반복적으로 수행해야 하는 설정 작업이 있을 때, "@BeforeEach" 를 사용하여 중복 코드를 줄일 수 있습니다.
독립적인 테스트 보장
테스트 간의 상호 의존성을 없애고, 각 테스트가 독립적으로 실행되도록 보장할 수 있습니다.
이를 통해 테스트 간에 상태가 공유되지 않도록 하여 신뢰성 있는 테스트를 구현할 수 있습니다.
"@BeforeEach" 는 테스트 환경을 일관되게 유지하고,
"@AfterEach" 애노테이션.
JUnit 5에서 각 테스트 매서드가 실행된 후에 실행되는 메서드를 붙이는 애노테이션 입니다.
이 메서드는 테스트가 완료된 후에 정리(clean-up) 작업을 수행하는 데 사용됩니다.
🙋♂️ 이 애노테이션의 주요 역할은 다음과 같습니다.
자원 정리
테스트 중에 사용된 자원(예: 파일, 데이터베이스 연결, 네트워크 연결 등)을 해제하거나 정리하는 데 사용됩니다.
이는 메모리 누수나 리소스 잠금을 방지할 수 있습니다.
테스트 환경 복원
테스트 실행 중에 변경된 상태나 데이터를 초기 상태로 되돌려, 다른 테스트에 영향을 미치지 않도록 합니다.
이는 테스트 간의 독립성을 유지하는 데 중요한 역할을 합니다.
로그 남기기
테스트가 끝난 후 테스트 결과나 상태에 대한 로그를 기록할 수 있습니다.
이를 통해 테스트 결과를 모니터링하거나 디버깅할 때 유용할 수 있습니다.
"@AfterEach" 는 테스트 후에 정리 작업을 자동으로 수행하여, 코드의 안정성과 유지보수성을 높이는 데 중요한 역할을 합니다.
Reflection
Reflection 은 자바에서 런타임 시에 클래스, 인터페이스, 메서드, 필드 등의 정보를 동적으로 조사하고, 조작할 수 있는 기능을 제공합니다.
Reflection 을 사용하면 코드에서 특정 객체의 클래스 타입이나 메서드, 필드 등에 접근하고, 해당 요소들을 동적으로 호출하거나 값을 변경하는 것이 가능합니다.
🙋♂️ Reflection의 주요 개념과 역할은 다음과 같습니다.
클래스 정보 조사
Reflection을 사용하면 특정 객체의 클래스 타입을 런타임에 알아낼 수 있습니다.
예를 들어, Class<?> clazz = obj.getClass(); 를 사용하여 객체 obj 의 클래스 정보를 가져올 수 있습니다.
필드, 메서드, 생성자 접근
Reflection을 통해 클래스에 선언된 필드, 메서드 ,생성자에 접근할 수 있습니다.
이를 통해 특정 필드의 값을 가져오거나 설정하고, 메서드를 호출하거나 생성자를 통해 객체를 생성할 수 있습니다.
예를 들어, Field field = clazz.getDeclaredField("fieldName);" 를 사용하여 특정 필드에 접근할 수 있습니다.
접근 제어 무시
Reflection을 사용하면 private 으로 선언된 필드나 메서드에도 접근할 수 있습니다. setAccessible(true) 메서드를 사용하여 접근 제어자를 무시할 수 있습니다.
이는 보통 테스트나 프레임워크에서 사용되며 예를 들어, 프레임워크에서 자동으로 의존성을 주입하거나, 테스트에서 private 필드에 접근할 때 유용합니다.
런타임에 동적 객체 생성 및 메서드 호출
Reflection을 사용하여 런타임에 동적으로 객체를 생성하거나 메서드를 호출할 수 있습니다.
이는 매우 유연한 코드 작성을 가능하게 하지만, 일반적으로 성능 저하가 있을 수 있습니다.
애노테이션 처리
Reflection을 사용하여 클래스나 메서드에 선언된 애노테이션을 런타임에 읽어들이고 처리할 수 있습니다.
이는 주로 프레임워크에서 사용되며, 예를 들어, 스프링 프레임워크에서 애노테이션 기반으로 설정을 처리하는 경우가 있습니다.
🙋♂️ Reflection의 장단점은 다음과 같습니다.
장점
동적 기능 제공 : 코드의 유연성과 확장성을 높여줍니다. 런타임에 클래스나 메서드를 동적으로 호출할 수 있어, 컴파일 타임에 알 수 없는 구조를 처리할 수 있습니다.
프레임워크에서 유용 : 많은 자바 프레임워크, 예를 들어 스프링(Spring), 하이버네이트(Hibernate) 등은 Reflection을 활용하여 애플리케이션의 동작을 제어합니다.
단점
성능 이슈 : Reflection은 일반적인 메서드 호출에 비해 성능이 떨어질 수 있습니다. 따라서 중요한 애플리케이션에서는 중의가 필요합니다.
안전성 문제 : Reflection을 사용하면 컴파일 타임에 확인할 수 없는 동작이 많아, 잘못 사용하면 런타임 에러가 발생할 수 있습니다.
보안 이슈 : Reflection은 접근 제어를 무시할 수 있으므로, 잘못된 사용은 보안상의 취약점을 초래할 수 있습니다.
4️⃣ 코드 설명.
Reflection을 사용하여 memberRepository 설정.
MemberService 의 memberRepository 필드는 private final 로 선언되어 있으므로, 일반적인 방식으로 접근할 수 없습니다.
이 문제를 해결하기 위해 Reflection 을 사용하여 필드에 접근하고, 테스트용 MemoryMemberRepository 를 설정합니다.
@BeforeEach 애노테이션.
각 테스트가 실행되기 전에 MemberService 인스턴스를 생성하고. Reflection 을 사용하여 memberRepository 필드를 MemoryMemberRepository 인스턴스로 초기화합니다.
@AfterEach 애노테이션.
각 테스트가 완료된 후, MemoryMemberRepository 의 저장소를 초기화하여 테스트 간의 상태 간섭을 방지합니다.
테스트 메서드.
회원가입 : 새로운 회원을 가입시키고, 저장된 회원이 올바르게 반환되는지 검증합니다.
중복_회원_제외 : 중복된 이름으로 회원을 가입하려 할 때 IllegalStateException 이 발생하는지를 확인합니다.
전체회원조회 : 저장된 모든 회원이 올바르게 반환되는지 검증합니다.
이 방법을 통해 MemberService 를 수정하지 않고도 해당 클래스의 동작을 테스트할 수 있습니다.
다만, 실제 코드에서는 생성자를 통한 주입이나 다른 테스트 가능한 구조로 변경하는 것이 더 바람직합니다.
-
☕️[Java] 테스트 코드와 Dependancy Injection
☕️[Java] 테스트 코드와 Dependancy Injection
1️⃣ 전체 코드.
// Member
public class Member {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
// MemberRepository - Interface
import com.devkobe.hello_spring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
// MemoryMemberRepository
import com.devkobe.hello_spring.domain.Member;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
}
}
// MemberService
import com.devkobe.hello_spring.domain.Member;
import com.devkobe.hello_spring.repository.MemberRepository;
import com.devkobe.hello_spring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
/*
* 회원 가입
*/
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
/*
* 전체 회원 조회
*/
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
// MemberServiceTest
import com.devkobe.hello_spring.domain.Member;
import com.devkobe.hello_spring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
@Test
void 회원가입() {
// given
Member member = new Member();
member.setName("spring");
// when
Long saveId = memberService.join(member);
// then
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
void findMembers() {
// given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
// when
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// then
}
@Test
void findOne() {
}
}
2️⃣ 의존성 주입(DI, Dependency Injection).
의존성 주입(DI, Dependency Injection) 는 객체 지향 프로그래밍에서 객체 간의 의존성을 외부에서 주입하는 디자인 패턴입니다.
1️⃣ 의존성 주입(DI, Dependency Injection)의 개념.
의존성(Dependency)
클래스가 다른 클래스의 기능을 사용해야 하는 상황을 의미합니다.
예를 들어, MemberService 클래스는 회원 데이터를 처리하기 위해 MemberRepository 를 필요로 합니다. 여기서 MemberService 는 MemberRepository 에 의존합니다.
주입(Injection)
외부에서 객체를 생성하여 주입해주는 것을 의미합니다.
이는 보통 생성자 주입, 세터(Setter) 주입, 필드 주입 등의 방식으로 이루어집니다.
2️⃣ DI의 주요 목적.
느슨한 결합(Loose Coupling)
DI를 통해 클래스 간의 결합도를 낮출 수 있습니다.
클래스는 자신이 사용하는 의존 객체가 무엇인지 알 필요가 없고, 이로 인해 클래스 간의 결합이 느슨해집니다.
이는 코드의 유연성을 높여주며, 코드 변셩 시 영향 범위를 줄여줍니다.
유연성 및 확장성
의존 객체를 외부에서 주입받기 때문에, 애플리케이션이 다른 의존 객체로 쉽게 전환될 수 있습니다.
예를 들어, 테스트 환경에서 MemoryMemberRepository 를 사용하고, 실제 운영 환경에서는 데이터베이스 연결을 사용하는 JpaMemberRepository 를 사용할 수 있습니다.
테스트 용이성
DI를 통해 클래스 간의 의존 관계를 외부에서 주입받으면, 테스트 시에 쉽게 Mock 객체나 Stub 객체를 주입할 수 있어 테스트를 더 용이하게 만듭니다.
3️⃣ DI의 사용 방법.
1. 생성자 주입(Constructor Injection) : 의존성을 클래스의 생성자를 통해 주입하는 방법입니다. 위 코드에서는 생성자 주입이 사용되었습니다.
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
2. 세터 주입(Setter Injection) : 의존성을 세터 메서드를 통해 주입하는 방법입니다.
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
3. 필드 주입(Field Injection) : 의존성을 필드에 직접 주입하는 방법으로, 보통 @Autowired 와 같은 애노테이션을 사용합니다. 하지만 필드 주입은 테스트하기 어려울 수 있어 일반적으로 권장되지 않습니다.
DI는 코드의 재사용성, 유지보수성, 테스트 용이성을 높여주는 중요한 디자인 패턴으로, 스프링 프레임워크와 같은 의존성 주입 컨테이너에서 널리 사용됩니다.
3️⃣ 코드에서 의존성 주입(DI, Dependency Injection)이 사용된 부분과 설명.
위 코드에서 DI(Dependency Injection)가 사용된 부분은 MemberService 클래스의 인스턴스를 생성하는 부분입니다.
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
이 부분에서 MemberService 클래스의 생성자에 MemoryMemberRepository 객체를 주입하고 있습니다.
MemberService 클래스는 생성자에서 MemberRepository 인터페이스를 받아 사용합니다.
아렇게 함으로써, MemberService 는 직접적으로 MemoryMemberRepository 를 생성하지 않고 외부에서 주입받게 됩니다.
1️⃣ 설명.
memberRepository = new MemoryMemberRepository();
MemoryMemberRepository 객체를 생성합니다. 이는 MemberRepository 인터페이스의 구현체입니다.
memberService = new MemberService(memberRepository);
MemberService 객체를 생성 할 때, 생성자에 memberRepository 를 주입합니다.
이 방식은 MemberService 가 MemoryMemberRepository 에 강하게 결합되지 않도록 하여, 다른 구현체로 쉽게 변경할 수 있게 만듭니다.
예를 들어, 테스트 환경에서는 MemoryMemberRepository 를 사용하고, 실제 서비스에서는 데이터베이스와 연결된 구현체를 사용할 수 있습니다.
-
-
💾 [CS] 의존성 주입(DI, Dependency Injection)
💾 [CS] 의존성 주입(DI, Dependency Injection).
1️⃣ 의존성 주입(DI, Dependency Injection)
싱글톤 패턴과 같이 사용하기 쉽고 굉장히 실용적이지만 모듈 간의 결합을 강하게 만들 수 있는 단점이 있는 패턴의 경우 의존성 주입(DI, Dependency Injection)을 통해 모듈 간의 결합을 조금 더 느슨하게 만들어 해결할 수 있습니다.
의존성이란 종속성이라고도 하며 A가 B에 의존성이 있다는 것은 B의 변경 사항에 대해 A 또한 변해야 된다는 것을 의미합니다.
앞의 그림처럼 메인 모듈(main module)이 ‘직접’ 다른 하위 모듈에 대한 의존성을 주기보다는 중간에 의존성 주입자(dependency injector)가 이 부분을 가로채 메인 모듈이 "간접적" 으로 의존성을 주입하는 방식입니다.
이를 통해 메인 모듈(상위 모듈)은 하위 모듈에 대한 의존성이 떨어지게 됩니다.
참고로 이를 ‘디커플링이 된다’ 고도 합니다.
1️⃣ 의존성 주입의 장점
모듈들을 쉽게 교체할 수 있는 구조가 되어 테스팅하기 쉽고 마이그레이션하기도 수월합니다.
또한, 구현할 때 추상화 레이어를 넣고 이를 기반으로 구현체를 넣어 주기 때문에 애플리케이션 의존성 방향이 일관되고, 애플리케이션을 쉽게 추론할 수 있으며, 모듈 간의 관계들이 조금 더 명확해집니다.
2️⃣ 의존성 주입의 단점
모듈들이 더욱더 분리되므로 클래스 수가 늘어나 복잡성이 증가될 수 있으며 약간의 런타임 페널티가 생기기도 합니다.
3️⃣ 의존성 주입 원칙
의존성 주입은 "상위 모듈은 하위 모듈에서 어떠한 것도 가져오지 않아야 합니다. 또한, 둘 다 추상화에 의존해야 하며, 이때 추상화는 세부 사항에 의존하지 말아야 합니다." 라는 의존성 주입 원칙을 지켜주면서 만들어야 합니다.
위 문장에서 “추상화”의 의미.
문장에서 “추상화”는 구체적인 구현에 의존하지 않고, 일반화된 인터페이스나 추상 클래스 등에 의존해야 한다는 것을 뜻합니다.
이 원칙은 의존성 역전 원칙(DIP, Dependency Inversion Principle) 과 관련이 있습니다.
상위 모듈 : 애플리케이션의 상위 계층에서 동작하는 코드, 즉 더 높은 수준의 정책이나 로직을 구현하는 모듈입니다.
하위 모듈 : 상위 모듈에서 호출하거나 사용하는 구체적인 기능이나 세부 사항을 구현하는 코드입니다.
1️⃣ 추상화.
“추상화” 는 객채 지향 프로그래밍(OOP)애서 중요한 개념 중 하나로, 구체적인 구현(details)을 감추고, 더 높은 수준의 개념을 정의하는 것을 의미합니다.
추상화는 구체적인 것보다는 더 일반적이고 보편적인 개념을 다루며, 특정한 구현 사항에 의존하지 않고 인터페이스나 추상 클래스 등을 통해 기능을 정의합니다.
2️⃣ 의존성 주입과 추상화의 관계.
의존성 주입(DI, Dependency Injection)은 의존성 역전 원칙(DIP, Dependency Inversion Principle)을 구현하기 위한 방법 중 하나입니다.
의존성 주입을 사용하면, 상위 모듈이 하위 모듈의 구체적인 구현에 의존하지 않고, 하위 모듈이 구현한 추상화(인터페이스나 추상 클래스)에 의존하도록 코드를 설계할 수 있습니다.
즉, 상위 모듈과 하위 모듈 모두 추상화된 인터페이스에 의존하게 하여, 구체적인 구현이 변경되더라도 상위 모듈의 코드가 영향을 받지 않도록 합니다.
3️⃣ 예시.
아래는 추상화와 의존성 주입을 적용한 예시입니다.
// 추상화된 인터페이스 (추상화)
public interface PaymentProcessor {
void processPayment(double amount);
}
// 하위 모듈 - 구체적인 구현
public class PayPalProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) {
// PayPal을 통해 결제 처리
}
}
public class CreditCardProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) {
// 신용카드를 통해 결제 처리
}
}
// 상위 모듈 - 추상화에 의존함
public class PaymentService {
private PaymentProcessor paymentProcessor;
// 의존성 주입을 통해 구현체를 주입 받음
public PaymentService(PaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
public void makePayment(double amount) {
paymentProcessor.processPayment(amount);
}
}
위 코드에서 "PaymentService" 는 "PaymentProcessor" 라는 추상화에 의존합니다.
"PaymentService" 는 "PayPalProcessor" 나 "CreditCardProcessor" 의 구체적인 구현을 알 필요가 없으며, 단지 "PaymentProcessor" 인터페이스에 정의된 메서드를 호출합니다.
- 이를 통해 결제 처리 방식이 PayPal에서 신용카드로 변경되더라도 "PaymentService" 는 수정할 필요가 없습나다.
이처럼 “추상화”는 상위 모듈과 하위 모듈이 특정 구현이 아닌, 일반적인 개념에 의존하도록 만들어줌으로써, 코드의 유연성과 재사용성을 높여주는 중요한 개념입니다.
-
-
☕️[Java] 공유 참조와 사이드 이펙트
☕️[Java] 공유 참조와 사이드 이펙트
1️⃣ 공유 참조와 사이드 이펙트.
사이드 이펙트(Side Effect)는 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 말합니다.
앞서 "b" 의 값을 부산으로 변경한 코드를 다시 분석해봅시다.
b.setValue("부산"); // b의 값을 부산으로 변경해야함
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이펙트 발생
System.out.println("b = " + b);
개발자는 "b" 의 주소값을 서울에서 부산으로 변경할 의도로 값 변경을 시도했습니다.
하지만 "a" , "b" 는 같은 인스턴스를 참조합니다.
따라서 "a" 의 값도 함께 부산으로 변경되어 버립니다.
이렇게 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 사이드 이펙트(Side Effect)라 합니다.
프로그래밍에서 사이드 이펙트는 보통 부정적인 의미로 사용되는데, 사이드 이펙트는 프로그래밍의 특정 부분에서 발생한 변경이 의도치 않게 다른 부분에 영향을 미치는 경우에 발생합니다.
이로 인해 디버깅이 어려워지고 코드의 안정성이 저하될 수 있습니다.
2️⃣ 사이드 이펙트 해결 방안
생각해보면 문제의 해결방안은 아주 단순합니다.
다음과 같이 "a" 와 "b" 가 처음부터 서로 다른 인스턴스를 참조하면 됩니다.
Address a = new Address("서울");
Address b = new Address("서울");
public class RefMain1_2 {
public static void main(String[] args) {
Address a = new Address("서울"); // x001
Address b = new Address("서울"); // x002
System.out.println("a = " + a);
System.out.println("b = " + b);
b.setValue("부산"); // b의 값을 부산으로 변경해야함
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이팩트 발생
System.out.println("b = " + b);
}
}
실행 결과
a = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='서울'}
b = Address{value='부산'}
실행 결과를 보면 "b" 의 주소값만 부산으로 변경된 것을 확인할 수 있습니다.
그림 - 생성 코드
"a" 와 "b" 는 서로 다른 "Address" 인스턴스를 참조합니다.
그림 - 변경 코드
"a" 와 "b" 는 서로 다른 인스턴스를 참조합니다.
따라서 "b" 가 참조하는 인스턴스의 값을 변경해도 "a" 에는 영향을 주지 않습니다.
3️⃣ 여러 변수가 하나의 객체를 공유하는 것을 막을 방법은 없다.
지금까지 발생한 모든 문제는 같은 객체(인스턴스)를 변수 "a" , "b" 가 함께 공유하기 때문에 발생했습니다.
따라서 객체를 공유하지 않으면 문제가 해결됩니다.
여기서 변수 "a", "b" 가 각각 다른 주소지로 변경할 수 있어야 합니다.
이렇게 하려면 서로 다른 객체를 참조하면 됩니다.
객체를 공유.
Address a = new Address("서울");
Address b = a;
이 경우 "a", "b" 둘 다 같은 "Address" 인스턴스를 바라보기 때문에 한 쪽의 주소만 부산으로 변경하는 것이 불가능합니다.
객체를 공유하지 않음.
Address a = new Address("서울");
Address ㅠ = new Address("서울");
이 경우 "a", "b" 는 서로 다른 "Address" 인스턴스를 바라보기 때문에 한 쪽의 주소만 부산으로 변경하는 것이 가능합니다.
이처럼 단순하게 서로 다른 객체를 참조해서, 같은 객체를 공유하지 않으면 문제가 해결됩니다.
즉, 여러 변수가 하나의 객체를 공유하지 않으면 지금까지 설명한 문제들이 발생하지 않습니다.
그런데 여기서 문제가 있습니다.
하나의 객체를 여러 변수가 공유하지 않도록 강제로 막을 수 있는 방법이 없다는 것입니다.
아래의 예시를 봐봅시다.
참조값의 공유를 막을 수 있는 방법이 없다.
Address a = new Address("서울");
Address b = a; // 참조값 대입을 막을 수 있는 방법이 없습니다.
"b = a" 와 같은 코드를 작성하지 않도록 해서, 여러 변수가 하나의 참조값을 공유하지 않으면 문제가 해결될 것 같습니다.
하지만 "Address" 를 사용하는 개발자 입장에서 실수로 "b = a" 라고 해도 아무런 오류가 발생하지 않습니다.
왜냐하면 자바 문법상 "Address b = a" 와 같은 참조형 변수의 대입은 아무런 문제가 없기 때문입니다.
다음과 같이 새로운 객체를 참조형 변수에 대입하든, 또는 기존 객체를 참조형 변수에 대입하든, 다음 두 코드 모두 자바 문법상 정상인 코드입니다.
Address b = new Address("서울"); // 새로운 객체 참조
Address b = a; // 기존 객체 공유 참조
참조값을 다른 변수에 대입하는 순간 여러 변수가 하나의 객체를 공유하게 됩니다.
즉, 객체의 공유를 막을 수 있는 방법이 없습니다!
기본형은 항상 값을 복사해서 대입하기 때문에 값이 절대로 공유되지 않습니다.
하지만 참조형의 경우 참조값을 복사해서 대입하기 때문에 여러 변수에서 얼마든지 같은 객체를 공유할 수 있습니다.
객체의 공유가 꼭 필요할 때도 있지만, 때로는 공유하는 것이 지금과 같은 사이드 이펙트를 만드는 경우도 있습니다.
물론 개발자가 신경써서 코드를 작성한다면 사이드 이펙트 문제를 일으키지 않을 수 있습니다.
하지만 실제로는 훨씬 더 복잡한 상황에서 이런 문제가 발생합니다.
```java
public class RefMain1_3 {
public static void main(String[] args) {
Address a = new Address(“서울”);
Address b = a;
System.out.println(“a = “ + a);
System.out.println(“b = “ + b);
change(b,"부산");
System.out.println("a = " + a); // 사이드 이팩트 발생
System.out.println("b = " + b); }
private static void change(Address address, String changeAddress) {
System.out.println(“주소 값을 변경합니다 -> “ + changeAddress);
address.setValue(changeAddress);
}
}
```
앞서 작성한 코드와 같은 코드입니다.
단순히 "change()" 메서드만 하나 추가되었습니다.
그리고 "change()" 메서드에서 "Address" 인스턴스에 있는 "value" 값을 변경합니다.
"main()" 메서드만 보면 "a" 의 값이 함께 부산으로 변경된 이유를 찾기가 더 어렵습니다.
실행 결과
a = Address{value='서울'}
b = Address{value='서울'}
주소 값을 변경합니다 -> 부산
a = Address{value='부산'}
b = Address{value='부산'}
-
☕️[Java] 불변 객체 - 도입
☕️[Java] 불변 객체 - 도입.
1️⃣ 불변 객체 - 도입.
공유하면 안되는 객체를 여러 변수에서 공유하기 때문에 문제가 발생했었습니다.
그렇다고 객체의 공유를 막을 수 있는 방법은 없습니다.
그러나 사이드 이펙트의 더 근본적인 원인을 고려해보면, 객체를 공유하는 것 자체는 문제가 아닙니다.
객체를 공유한다고 바로 사이드 이펙트가 발생하지 않습니다.
문제의 직접적인 원인은 공유된 객체의 값을 변경한 것에 있습니다.
"a", "b" 는 처음 시점에는 둘 다 "서울" 이라는 주소를 사용해야 합니다.
그리고 이후에 "b" 의 주소를 "부산" 으로 변경해야 합니다.
Address a = new Address("서울");
Address b = a;
따라서 처음에는 "b = a" 와 같이 "서울" 이라는 "Address" 인스턴스를 "a", "b" 가 함께 사용하는 것이, 다음 코드와 같이 서로 다른 인스턴스를 사용하는 것 보다 메모리와 성능상 더 효율적입니다.
인스턴스가 하나이니 메모리가 절약되고, 인스턴스를 하나 생성하지 않아도 되니 생성 시간이 줄어서 성능상 효율적입니다.
Address a = new Address("서울");
Address b = new Address("서울");
여기까지는 "Address b = a" 와 같이 공유 참조를 사용해도 아무런 문제가 없습니다.
오히려 더 효율적입니다.
진짜 문제는 이후에 b가 공유 참조하는 인스턴스의 값을 변경하기 때문에 발생합니다.
b.setValue("부산"); // b의 값을 부산으로 변경해야함
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이펙트 발생
System.out.println("b = " + b);
자바에서 여러 참조형 변수가 하나의 객체(인스턴스)를 참조하는 공유 참조 문제는 피할 수 없습니다.
기본형과 다르게 참조형인 객체는 처음부터 여러 참조형 변수에서 공유될 수 있도록 설계되었습니다.
따라서 이것은 문제가 아닙니다.
문제의 직접적인 원인은 공유될 수 있는 Address 객체의 값을 어디선가 변경했기 때문입니다.
만약 "Address" 객체의 값을 변경하지 못하게 설계했다면 이런 사이드 이펙트 자체가 발생하지 않을 것입니다.
2️⃣ 불변 객체 도입.
객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라 합니다.
"Address" 클래스를 상태가 변하지 않는 불변 클래스로 만들어 보겠습니다.
public class ImmutableAddress {
private final String value;
public ImmutableAddress(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return "Address{" +
"value='" + value + '\'' +
'}';
}
}
내부 값이 변경되면 안됩니다.
따라서 "value" 의 필드를 "final" 로 선언했습니다.
값을 변경할 수 있는 "setValue()" 를 제거했습니다.
이 클래스는 생성자를 통해서만 값을 설정할 수 있고, 이후에는 값을 변경하는 것이 불가능합니다.
불변 클래스를 만드는 방법은 아주 단순합니다.
어떻게든 필드 값을 변경할 수 없게 클래스를 설계하면 됩니다.
```java
public class RefMain2 {
public static void main(String[] args) {
ImmutableAddress a = new ImmutableAddress(“서울”);
ImmutableAddress b = a; // 참조값 대입을 막을 수 있는 방법이 없다.
System.out.println(“a = “ + a);
System.out.println(“b = “ + b);
// b.setValue("부산"); // 컴파일 오류 발생
b = new ImmutableAddress("부산");
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이팩트 발생
System.out.println("b = " + b); } } ```
"ImmutableAddress" 의 경우 값을 변경할 수 있는 "b.setValue()" 메서드 자체가 제거되었습니다.
이제 "ImmutableAddress" 인스턴스의 값을 변경할 수 있는 방법은 없습니다.
"ImmutableAddress" 를 사용하는 개발자는 값을 변경하려고 시도하다가, 값을 변경하는 것이 불가능하다는 사실을 알고, 이 객체가 불변 객체인 사실을 깨닫게 됩니다.
예를 들어 "b.setValue("부산")" 을 호출하려고 했는데, 해당 메서드가 없다는 사실을 컴파일 오류를 통해 인지합니다.
따라서 어쩔 수 없이 새로운 "ImmutableAddress("부산")" 인스턴스를 생성해서 "b" 에 대입합니다.
결과적으로 "a", "b" 는 서로 다른 인스턴스를 참조하고, "a" 가 참조하던 "ImmutableAddress" 는 그대로 유지됩니다.
실행 결과
a = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='서울'}
b = Address{value='부산'}
실행 결과를 보면 "a" 의 값은 그대로 유지되는 것을 확인할 수 있습니다.
자바에서 객체의 공유 참조는 막을 수 없습니다.
"ImmutableAddress" 는 불변 객체입니다.
따라서 값을 변경할 수 없습니다.
"ImmutableAddress" 은 불변 객체이므로 "b" 가 참조하는 인스턴스의 값을 서울에서 부산으로 변경하려면 새로운 인스턴스를 생성해서 할당해야 합니다.
3️⃣ 정리
불변이라는 단순한 제약을 사용해서 사이드 이펙트라는 큰 문제를 막을 수 있습니다.
객체의 공유 참조는 막을 수 없습니다.
그래서 객체의 값을 변경하면 다른 곳에서 참조하는 변수의 값도 함께 변경되는 사이드 이펙트가 발생합니다.
사이드 이펙트가 발생하면 안되는 상황이라면 불변 객체를 만들어서 사용하면 됩니다.
불변 객체는 값을 변경할 수 없기 때문에 사이드 이펙트가 원천 차단됩니다.
불변 객체는 값을 변경할 수 없습니다.
따라서 불변 객체의 값을 변경하고 싶다면 변경하고 싶은 값으로 새로운 불변 객체를 생성해야 합니다.
이렇게 하면 기존 변수들이 참조하는 값에는 영향을 주지 않습니다.
🙋♂️ 참고 - 가변(Mutable) 객체 vs 불변(Immutable) 객체
가변은 이름 그대로 처음 만든 이후 상태가 변할 수 있다는 뜻입니다.
가변은 사전적으로 사물의 모양이나 성질이 달라질 수 있다는 뜻입니다.
불변은 이름 그대로 처음 만든 이후 상태가 변하지 않는다는 뜻입니다.
불변은 사전적으로 사물의 모양이나 성질이 달라질 수 없다는 뜻입니다.
"Address" 는 가변 클래스입니다.
이 클래스로 객체를 생성하면 가변 객체가 됩니다.
"ImmutableAddress" 는 불변 클래스입니다.
이 클래스로 객체를 생성하면 불변 객체가 됩니다.
-
-
-
-
☕️[Java] Object와 OCP - 2
☕️[Java] Object와 OCP - 2.
1️⃣ Object.
Java에서 'Object' 는 모든 클래스의 최상위 부모 클래스이자, Java 클래스 계층 구조의 최상위에 위치하는 클래스입니다.
Java에서 모든 클래스는 암묵적으로 'Object' 클래스를 상속받으며, 이로 인해 'Object' 클래스가 제공하는 메서드를 사용할 수 있습니다.
이는 Java의 객체 지향 프로그래밍(OOP)에서 중요한 역할을 합니다.
1️⃣ Object 클래스의 주요 역할.
1. 최상위 클래스.
모든 Java 클래스는 'Objcet' 클래스를 상속받기 때문에 'Object' 클래스에서 제공하는 메서드는 모든 객체에서 사용할 수 있습니다.
2. 기본 메서드 제공.
'Object' 클래스는 모든 객체가 기본적으로 사용할 수 있는 몇 가지 중요한 메서드를 제공합니다.
예를 들어 다음과 같습니다.
'equals(Object obj)'
두 객체가 같은지를 비교합니다.
'hashCode()'
객체의 해시 코드를 반환합니다. 이 값은 객체를 식별하는 데 사용됩니다.
'toString()'
객체를 문자열로 표현합니다.
'clone()'
객체를 복제합니다.(단, 클래스에서 'Cloneable' 인터페이스를 구현해야 사용 가능)
'finalize()'
객체가 가비지 컬렉션되기 전에 호출됩니다.
3. 다형성 지원.
'Object' 타입으로 모든 객체를 참조할 수 있으므로, 다양한 객체를 처리하는 메서드나 컬렉션에서 유연성을 제공합니다.
예를 들어, Java의 컬렉션 프레임워크에서는 'Object' 타입을 사용하여 다양한 타입의 객체를 저장할 수 있습니다.
4. 공통 기능의 확장.
모든 클래스가 'Object' 를 상속받기 때문에, 'Object' 클래스의 메서드를 재정의(Override)하여 클래스에 맞는 동작을 구현할 수 있습니다.
예를 들어, 'toString()' 메서드를 재정의하여 객체의 상태를 의미 있는 문자열로 표현할 수 있습니다.
2️⃣ 예시.
다음은 'Object' 클래스의 메서드를 활용하는 간단한 예시입니다.
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Person person = (Person) obj;
return age == person.age && name.equals(person.name);
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
public static void main(String[] args) {
Person person1 = new Person("Alice", 30);
Person person2 = new Person("Alice", 30);
System.out.println(person1.equals(person2)); // true
System.out.println(person1.toString()); // Person{name='Alice', age=30}
}
}
이 예시에서 'equals' 와 'toString' 메서드는 'Object' 클래스에서 제공하는 메서드를 재정의하여 'Person' 클래스의 객체에 적합한 동작을 정의하고 있습니다.
3️⃣ 결론.
Java의 'Object' 클래스는 모든 클래스의 공통 조상으로서 중요한 역할을 하며, 기본적인 객체 비교, 해시 코드 생성, 문자열 표현 등의 기능을 제공합니다.
이는 Java에서의 객체 지향 프로그래밍의 기초를 이루며, 다양한 클래스 간의 상호작용을 가능하게 합니다.
2️⃣ OCP
OCP는 “Open/Closed Principle”의 약자로, 객체 지향 설계의 중요한 원칙 중 하나입니다.
이 원칙은 Robert C. Martin에 의해 정의된 SOLID 원칙 중 하나로, 소프트웨어 설계의 유연성과 유지보수성을 높이기 위한 지침을 제공합니다.
1️⃣ OCP(Open/Closed Principle)의 정의
“소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 열려 있어야 하고, 수정에는 닫혀 있어야 한다.” 라는 원칙입니다.
확장에 열려 있어야 한다(Open for extension)
새로운 기능이나 요구사항이 추가될 때, 기존 코드를 변경하지 않고도 기능을 확장할 수 있어야 한다는 뜻입니다.
이를 통해 소프트웨어를 유연하게 확장할 수 있으며, 새로운 기능을 도입할 때 기존 코드에 영향을 주지 않게 됩니다.
수정에 닫혀 있어야 한다(Closed for modification)
기존에 잘 작동하던 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어야 한다는 뜻입니다.
이는 소프트웨어의 안정성을 유지하면서 변경의 영향을 최소화할 수 있습니다.
2️⃣ OCP의 구현 방법.
OCP를 구현하는 가장 일반적인 방법은 추상화 와 다형성 을 사용하는 것입니다.
추상 클래스나 인터페이스를 통해 기본 구조를 정의하고, 이를 상속하거나 구현하여 구체적인 기능을 확장합니다.
이렇게 하면 기존 코드베이스를 변경하지 않고도 새로운 기능을 추가할 수 있습니다.
3️⃣ 예시.
// 기존 코드: Shape 인터페이스 정의
interface Shape {
double calculateArea();
}
// 기존 코드: Rectangle 클래스 정의
class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
}
// 확장 코드: Circle 클래스 정의 (기존 코드를 수정하지 않음)
class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
// 클라이언트 코드
public class Main {
public static void main(String[] args) {
Shape rectangle = new Rectangle(10, 20);
Shape circle = new Circle(5);
System.out.println("Rectangle Area: " + rectangle.calculateArea());
System.out.println("Circle Area: " + circle.calculateArea());
}
}
설명
이 예시에서 'Shape' 인터페이스는 면적을 계산하는 'calculateArea()' 메서드를 정의합니다.
'Rectangle' 클래스는 이 인터페이스를 구현하여 사각형의 면적을 계산하는 기능을 제공합니다.
이후, 'Circle' 클래스를 추가하면서 새로운 기능을 확장합니다.
이 과정에서 기존의 'Rectangle' 클래스나 'Shape' 인터페이스의 코드는 전혀 수정하지 않고 새로운 기능을 추가할 수 있었습니다.
이처럼 OCP를 잘 준수하면 소프트웨어가 변화하는 요구사항에 유연하게 대응할 수 있으며, 유지보수 비용을 줄이고 코드의 재사용성을 높일 수 있습니다.
3️⃣ Object와 OCP의 관계.
'Object' 와 'OCP(Open/Closed Principle)' 는 둘 다 객체 지향 프로그래밍의 개념과 밀접하게 관련되어 있지만, 그 역할과 목적은 다릅니다.
이 둘의 관계ㄴ를 이해하려면 먼저 각각의 역할을 간단히 요약하고, 그 후에 이들이 어떻게 상호작용하는지 설명할 수 있습니다.
1️⃣ ‘Object’ 클래스의 역할
Java에서 'Object' 클래스는 모든 클래스의 최상위 부모 클래스입니다.
모든 Java 클래스는 암묵적으로 'Object' 를 상속받으며, 'Object' 클래스에서 제공하는 메서드를 사용할 수 있습니다.
이 클래스는 Java에서 객체의 기본적인 기능(예: 'equalse', 'hashCode', 'toString')을 제공합니다.
'Object' 클래스 자체는 특정한 설계 원칙을 강제하지 않지만, 객체 지향 프로그래밍의 근본적인 기초를 제공합니다.
2️⃣ OCP(Open/Closed Principle)의 역할.
OCP는 소프트웨어 설계 원칙으로, 소프트웨어가 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다 는 원칙을 따릅니다.
이 원틱은 소프트웨어를 설계할 때 변화하는 요구사항에 유연하게 대응하고, 코드의 수정 없이도 기능을 확장할 수 있도록 구조화하는 방법론입니다.
이는 주로 인터페이스, 추상 클래스, 상속, 다형성 등을 통해 구현됩니다.
3️⃣ ‘Object’와 OCP의 관계.
1. 기본 클래스 구조 제공.
‘Object’ 클래스는 모든 클래스의 기본이 되며, OCP 원칙을 적용하기 위한 기본 구조를 제공합니다.
예를 들어, 모든 클래스가 'Object' 를 상속받기 때문에, 클래스 설계자는 'Object' 클래스에서 제공하는 메서드(예: 'equals', 'hashCode') 를 재정의할 수 있습니다.
이를 통해 각 클래스가 자신만의 독특한 행동을 가지도록 확장할 수 있습니다.
2. 추상화와 다형성의 기초.
OCP를 실현하기 위해서는 추상화와 다형성이 중요한데, 'Object' 클래스는 이 둘의 기초를 제공합니다.
모든 클래스는 'Object' 타입으로 참조될 수 있으므로, OCP를 구현할 때 다형성을 활용할 수 있습니다.
예를 들어, 다양한 타입의 객체를 'Object' 로 처리하면서도, 각 객체가 특정 행위를 다르게 구현하도록 설계할 수 있습니다.
3, OCP 준수에 대한 도움.
'Object' 클래스는 모든 클래스가 공통적으로 가져야 하는 기본적인 기능을 제공하기 때문에, 설계자는 이 기능을 바탕으로 필요한 부분만 재정의하여 기능을 확장할 수 있습니다.
이는 OCP를 준수하는 데 도움이 됩니다.
예를 들어, 특정 객체가 'equals' 메서드를 새롭게 구현함으로써 비교 방식을 확장하면서도, 기존의 'Object' 클래스 코드를 수정할 필요는 없습니다.
4️⃣ 결론.
'Object' 클래스는 모든 클래스의 기반이 되며, OCP를 준수하는 소프트웨어 설계에 있어 중요한 역할을 합니다.
Object 는 직접적으로 OCP를 구현하지 않지만, 그 위에서 개발자들이 OCP 원칙을 적용할 수 있도록 추상화와 다형성의 기초를 제공합니다.
OCP를 준수하는 설계를 통해 개발자는 객체 지향 프로그래밍의 이점을 극대화할 수 있습니다.
-
☕️[Java] Object와 OCP - 1
☕️[Java] Object와 OCP - 1.
1️⃣ Object와 OCP.
만약 'Object' 가 없고, 또 'Object' 가 제공하는 'toString()' 이 없다면 서로 아무 관계가 없는 객체의 정보를 출력하기 어려울 것입니다.
여기서 아무 관계가 없다는 것은 공통의 부모가 없다는 뜻입니다.
아마도 다음의 'BadObjectPrinter' 클래스와 같이 각각의 클래스마다 별도의 메서드를 작성해야 할 것입니다.
'BadObjectPrinter'
public class BadObjectPrinter {
public static void print(Car car) { // Car 전용 메서드
String string = "객체 정보 출력: " + car.carInfo(); // carInfo() 메서드 만듬
System.out.println(string);
}
public static void print(Dog dog) { // Dog 전용 메서드
String string = "객체 정보 출력: " + dog.dogInfo(); //dogInfo() 메서드 만듬
System.out.println(string);
}
}
1️⃣ 구체적인 것에 의존.
'BadObjectPrinter' 는 구체적인 타입인 'Car' , 'Dog' 를 사용합니다.
따라서 이후에 출력해야 할 구체적인 클래스가 10개로 늘어나면 구체적인 클래스에 맞추어 메서드도 10개로 계속 늘어나게 됩니다.
이렇게 'BadObjectPrinter' 클래스가 구체적인 특정 클래스인 'Car' , 'Dog' 를 사용하는 것을 'BadObjectPrinter' 는 'Car' , 'Dog' 에 의존한다고 표현합니다.
자바에는 객체의 정보를 사용할 때, 다형적 참조 문제를 해결해줄 'Object' 클래스와 메서드 오버라이딩 문제를 해결해줄 'Object.toString()' 메서드가 있습니다.
(물론 직접 'Object' 와 비슷한 공통의 부모 클래스를 만들어서 해결할 수도 있습니다.)
2️⃣ 추상적인 것에 의존.
public class ObjectPrinter {
public static void print(Object obj) {
String string = "객체 정보 출력: " + obj.toString();
System.out.println(string);
}
}
위의 'ObjectPrinter' 클래스는 'Car' , 'Dog' 같은 구체적인 클래스를 사용하는 것이 아니라, 추상적인 'Object' 클래스를 사용합니다.
이렇게 'ObjectPrinter' 클래스가 'Object' 클래스를 사용하는 것을 'Object' 클래스에 의존한다고 표현합니다.
'ObjectPrinter' 는 구체적인 것에 의존하는 것이 아니라 추상적인 것에 의존합니다.
추상적 : 여기서 말하는 추상적이라는 뜻은 단순히 추상 클래스나 인터페이스만 뜻하는 것은 아닙니다.
Animal 과 Dog , Cat 의 관계를 떠올려봅시다.
Animal 같은 부모 타입으로 올라갈 수록 개념은 더 추상적이게 되고, Dog , Cat 과 같이 하위 타입으로 내려갈 수록 개념은 더 구체적이게 됩니다.
'ObjectPrinter' 와 'Object' 를 사용하는 구조는 다형성을 매우 잘 활용하고 있습니다.
다형성을 잘 활용한다는 것은 다형적 참조와 메서드 오버라이딩을 적절하게 사용한다는 뜻입니다.
🤔 ObjectPrinter 의 print() 메서드와 전체 구조 분석.
다형적 참조
'print(Object obj)' , 'Object' 타입을 매개변수로 사용해서 다형적 참조를 사용합니다.
'Car' , 'Dog' 인스턴스를 포함한 세상의 모든 객체 인스턴스를 인수로 받을 수 있습니다.
메서드 오버라이딩
'Object' 는 모든 클래스의 부모입니다.
따라서 'Dog' , 'Car' 와 같은 구체적인 클래스는 'Object' 가 가지고 있는 'toString()' 메서드를 오버라이딩 할 수 있습니다.
따라서 print(Object obj) 메서드는 Dog , Car 와 같은 구체적인 타입에 의존(사용)하지 않고, 추상적인 Object 타입에 의존하면서 런타임에 각 인스턴스의 'toString()' 을 호출할 수 있습니다.
3️⃣ OCP 원칙.
Open : 새로운 클래스를 추가하고, 'toString()' 을 오버라이딩해서 기능을 확장할 수 있습니다.
Closed : 새로운 클래스를 추가해도 'Object' 와 'toString()' 을 사용하는 클라이언트 코드인 'ObjectPrinter' 는 변경하지 않아도 됩니다.
다형적 참조, 메서드 오버라이딩, 그리고 클라이언트 코드가 구체적인 'Car', 'Dog' 에 의존하는 것이 아니라 추상적인 'Object' 에 의존하면서 OCP 원칙을 지킬 수 있었습니다.
덕분에 새로운 클래스를 추가하고 toString() 메서드를 새롭게 오버라이딩해서 기능을 확장할 수 있습니다.
그리고 이러한 변화에도 불구하고 클라이언트 코드인 ObjectPrinter 는 변경할 필요가 없습니다.
'ObjectPrinter' 는 모든 타입의 부모인 'Object' 를 사용하고, 'Object' 가 제공하는 'toString()' 메서드만 사용합니다.
따라서 'ObjectPrinter' 를 사용하면 세상의 모든 객체의 정보('toString()') 를 편리하게 출력할 수 있습니다.
2️⃣ System.out.println()
지금까지 설명한 'ObjectPrinter.print()' 는 사실 'System.out.println()' 의 작동 방식을 설명하기 위해 만든 것입니다.
'System.out.println()' 메서드도 'Object' 매개변수를 사용하고 내부에서 'toString()' 을 호출합니다.
따라서 'System.out.println()' 를 사용하면 세성의 모든 객체의 정보('toString()')를 편리하게 출력할 수 있습니다.
자바 언어는 객체지향 언어 답게 언어 스스로도 객체지향의 특징을 매우 잘 활용합니다.
우리가 지금까지 배운 'toString()' 메서드와 같이, 자바 언어가 기본으로 제공하는 다양한 메서드들은 개발자가 필요에 따라 오버라이딩해서 사용할 수 있도록 설계되어 있습니다.
참고 - 정적 의존관계 vs 동적 의존관계
정적 의존관계 는 컴파일 시간에 결정되며, 주로 클래스 간의 관계를 의미합니다.
앞서 보여준 클래스 의존 관계 그림이 바로 정적 의존관계입니다.
쉽게 이야기해서 프로그램을 실행하지 않고, 클래스 내에서 사용하는 타입들만 보면 쉽게 의존관계를 파악할 수 있습니다.
동적 의존관계 는 프로그램을 실행하는 런타임에 확인할 수 있는 의존관계입니다.
앞서 'ObjectPrinter.print(Object obj)' 에 인자로 어떤 객체가 전달 될 지는 프로그램을 실행해봐야 알 수 있습니다.
어떤 경우에는 'Car' 인스턴스가 넘어오고, 어떤 경우에는 'Dog' 인스턴스가 넘어옵니다.
이렇게 런타임에 어떤 인스턴스를 사용하는지를 나타내는 것이 동적 의존관계입니다.
참고로 단순히 의존관계 또는 어디에 의존한다고 하면 주로 정적 의존관계를 뜻합니다.
예) 'ObjectPrinter' 는 'Object' 에 의존합니다.
-
-
🍃[Spring] 일반적인 웹 애플리케이션 계층 구조와 클래스 의존관계.
🍃[Spring] 일반적인 웹 애플리케이션 계층 구조와 클래스 의존관계.
1️⃣ 일반적인 웹 애플리케이션 계층 구조.
Controller : 웹 MVC의 Controller 역할.
사용자의 요청을 받아 이를 처리할 비즈니스 로직(서비스 레이어)에 전달하고, 그 결과를 다시 사용자에게 응답하는 역할을 합니다.
주로 HTTP 요청을 처리하고, 올바른 응답을 생성합니다.
컨트롤러는 사용자로부터 입력을 받아 해당 입력을 서비스 레이어로 전달하고, 서비스 레이어에서 처리된 결과를 사용자에게 반환합니다.
이는 주로 웹 애플리케이션의 엔트포인트(예: '/login', '/signup' 와 같은 URL)에 대응됩니다.
Service : 핵심 비즈니스 로직 구현.
비즈니스 로직을 처리하는 계층입니다.
컨트롤러와 리포지토리 사이에서 중간 역할을 하며, 여러 리포지토리로부터 데이터를 가져오거나 가공하고, 이를 다시 컨트롤러에 전달합니다.
서비스 계층은 애플리케이션의 핵심 비즈니스 로직이 위치하는 곳입니다.
예를 들어, 사용자 인증, 결제 처리, 이메일 전송 등의 주요 기능이 이 계층에서 처리됩니다.
Repository: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리.
데이터베이스와 상호작용하는 계층입니다.
데이터의 저장, 검색, 갱신, 삭제 등의 작업을 처리하며, 데이터베이스와의 직접적인 통신을 담당합니다.
리포지토리는 데이터를 처리하기 위한 SQL 쿼리나 ORM(Object-Relational Mapping) 작업을 담당합니다.
이 계층은 서비스 계층에서 필요한 데이터를 가져오거나, 새 데이터를 저장하는 역할을 합니다.
Domain: 비즈니스 도메인 객체.
예를 들어 회원, 주문 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨.
애플리케이션의 핵심 엔티티(Entity)와 비즈니스 규칙을 정의하는 계층입니다.
보통 객체로 표현되며, 비즈니스 로직의 일부를 캡슐화합니다.
도메인 계층은 애플리케이션에서 중요한 객체들(예: 'User', 'Product', 'Order' 등)을 정의하고, 이 객체들이 어떤 방식으로 상호작용하는지를 나타냅니다.
이는 애플리케이션이 어떤 비즈니스 문제를 해결하는지에 대한 모델을 나타냅니다.
2️⃣ 클래스 의존관계.
회원 비즈니스 로직에는 회원 서비스가 있다.
회원을 저장하는 것은 인터페이스로 설계 되어있다.
그 이유는 아직 데이터 저장소가 선정되지 않았음을 가정하고 설계했기 때문이다.
그리고 구현체를 우선은 메모리 구현체로 만들것이다.
그 이유는 일단 개발은 해야하므로 굉장히 단순한 메모리 기반의 데이터 저장소를 사용하여 메모리 구현체로 만든다.
향후에 메모리 구현체를 구체적인 기술이 선정이 되면(RDB, NoSQL 등) 교체할 것이다.
교체하려면 Interface가 필요하므로 Interface를 정의한 것이다.
아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계
데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민중인 상황으로 가정
개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용
-
-
-
🌐[Network] 인터네트워킹
🌐[Network] 인터네트워킹
네트워크와 네트워크의 연결을 인터네트워킹(Internetworking)이라 하며, 연결되는 네트워크 수가 증가할수록 복잡도가 커집니다.
인터넷은 IP 프로토콜을 지원하는 전 세계의 모든 네트워크가 반복 구조로 연결된 시스템을 의미하며, 라우터라는 중개 장비를 사용해서 네트워크들을 연결합니다.
1️⃣ 네트워크의 연결
위 그림처럼 서로 독립적으로 운영되는 2개 이상의 네트워크가 연동되어 정보를 교환하려면, 이를 적절히 연결하여 데이터를 중개할 수 있는 인터네트워킹 시스템이 필요합니다.
여기에서 네트워크가 연동된다는 의미는 물리적인 연결뿐 아니라, 데이터 중개에 필요한 상위의 네트워크 프로토콜들이 지원됨을 뜻합니다.
인터넷에서 인터네트워킹 시스템의 주요 기능은 전송 데이터의 경로 선택과 관계가 있습니다.
예를 들어 위 그림의 네트워크 1에서 유입된 데이터를 네트워크 2와 네트워크 3의 누구에게 보낼 것인가를 선택해야 합니다.
이 기능은 7계층 모델에서 네트워크 계층에 포함되므로 인터네트워킹 시스템은 네트워크 계층을 포함한 하위 3개 계층의 기능을 수행합니다.
그림의 예는 일반 도로로 비유하자면 삼거리의 경우와 유사하며, 여기서 자동차 내비게이션 기능이 인터네트워킹 시스템의 주요 기능에 해당합니다.
인터넷의 내부 구조는 이와 같은 인터네트워킹 시스템들이 복잡하게 연결되어 상호 유기적인 협조 체제로 동작합니다.
인터네트워킹 시스템에 연결된 네트워크들은 물리적으로 같은 종류일 필요가 없으며, 상위 계층 프로토콜들이 지원하는 논리적 기능도 다를 수 있습니다.
하지만 인터네트워킹 시스템은 연결된 모든 네트워크에 대하여 물리적이고 논리적인 인터페이스를 모두 지원해야 합니다.
즉, 위 그림에서 인터네트워킹 시스템은 네트워크 1, 네트워크 2, 네트워크 3과 개별적으로 연동할 수 있어야 합니다.
또한 이 과정에서 데이터 표현 방식을 포함해 양쪽 네트워크의 프로토콜이 서로 일치하지 않으면 필요한 변환 작업을 수행해야 합니다.
이러한 방식으로 인터네트워킹 시스템은 둘 이상의 네트워크를 유기적으로 연동할 수 있습니다.
2️⃣ 게이트웨이
인터네트워킹 시스템은 용어 자체로 의미를 쉽게 설명하고 이해시키기 위한 개념적인 명칭이며, 인터네트워킹 기능을 수행하는 시스템을 일반적으로 게이트웨이(Gateway)라 부릅니다.
게이트웨이의 종류는 다양하지만, 일반적으로 지원할 수 있는 기능의 한계에 따라 리피터, 브리지, 라우터 등으로 나뉩니다.
1️⃣ 리피터(Repeater)
리피터(Repeater)는 물리 계층의 기능을 지원합니다.
물리적 신호는 전송 거리가 멀수록 감쇄되기 때문에 중간에 이를 보완해주어야 합니다.
예를 들어, 사람의 목소리는 멀리 전달될수록 세기가 약해져서 점점 알아들을 수 없게 됩니다.
이와 같이 네트워크에서도 무선 신호 혹은 유선의 전기적 신호도 거리가 멀어질수록 신호의 크기가 약해집니다.
따라서 리피터는 한쪽에서 입력된 신호를 물리적으로 단순히 증폭하여 다른 쪽으로 중개하는 역할을 합니다.
2️⃣ 브리스(Bridge)
리피터는 단순히 신호를 증폭하는 역할을 하며, 전송과정에서 발생하는 물리적인 오류 문제는 다루지 않습니다.
이를 보완한 브리지(Bridge)는 리피터 기능에 데이터 링크 계틍의 기능이 추가 된 것으로 물리 계층에서 발생한 오류를 해결해줍니다.
예를 들어, 가정에서 사용하는 무선 공유기는 유무선 기능을 모두 지원하는 브리지의 예입니다.
3️⃣ 라우터(Router)
라우터(Router)는 물리 계층, 데이터 링크 계층, 네트워크 계층의 기능을 지원합니다.
네트워크 계층은 경로 선택 기능을 제공해야 하므로 임의의 네트워크에서 들어온 데이터를 어느 네트워크로 전달할지 판단할 수 있어야 합니다.
이를 위하여 라우터는 자신과 연결된 네트워크와 호스트들의 정보를 유지,관리함으로써 특정 경로가 이용 가능한지 여부와 다수의 경로 중에서 어느 경로가 빠른 데이터 전송을 지원하는지 판단할 수 있어야 합니다.
네트워크와 호스트에 대한 정보는 일반적으로 라우팅 테이블(Routing Table)에 보관됩니다.
-
-
☕️[Java] toString()
☕️[Java] toString()
1️⃣ toString()
'Object.toString()' 메서드는 객체의 정보를 문자열 형태로 제공합니다.
그래서 디버깅과 로깅에 유용하게 사용됩니다.
이 메서드는 'Object' 클래스에 정의되므로 모든 클래스에서 상속받아 사용할 수 있습니다.
package langReview.object.tostring;
public class ToStringMain1 {
public static void main(String[] args) {
Object object = new Object();
String string = object.toString();
// toString() 반환값 출력
System.out.println(string);
// object 직접 출력
System.out.println(object);
}
}
실행 결과
java.lang.Object@b4c966a
java.lang.Object@b4c966a
1️⃣ Object.toString()
'Object' 가 제공하는 'toString()' 메서드는 기본적으로 패키지를 포함한 객체의 이름과 객체의 참조값(해시코드)를 16진수로 제공합니다.
2️⃣ println()과 toString()
'toString()' 의 결과를 출력한 코드와 'object' 를 'println()' 에 직접 출력한 코드의 결과가 완전히 같습니다.
Object object = new Object();
String string = object.toString();
// toString() 반환값 출력
System.out.println(string);
// object 직접 출력
System.out.println(object);
'System.out.println' 메서드는 사실 내부에서 'toString()' 을 호출합니다.
'Object' 타입(자식 포함)이 'println()' 에 인수로 전달되면 내부에서 'obj.toString()' 메서드를 호출해서 결과를 출력합니다.
따라서 'println()' 을 사용할 때, 'toString()' 을 직접 호출할 필요 없이 객체를 바로 전달하면 객체의 정보를 출력할 수 있습니다.
2️⃣ toString() 오버라이딩.
'Object.toString()' 메서드가 클래스 정보와 참조값을 제공하지만 이 정보만으로는 객체의 상태를 적절히 나타내지 못합니다.
그래서 보통 'toString()' 을 재정의(Overriding, 오버라이딩)해서 보다 유용한 정보를 제공하는 것이 일반적입니다.
package langReview.object.tostring;
public class Car {
private String carName;
public Car(String carName) {
this.carName = carName;
}
}
package langReview.object.tostring;
public class Dog {
private String dogName;
private int age;
public Dog(String dogName, int age) {
this.dogName = dogName;
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"dogName='" + dogName + '\'' +
", age=" + age +
'}';
}
}
package langReview.object.tostring;
public class ObjectPrinter {
public static void print(Object obj) {
String string = "객체 정보 출력: " + obj.toString();
System.out.println(string);
}
}
package langReview.object.tostring;
public class ToStringMain2 {
public static void main(String[] args) {
Car car = new Car("Model Y");
Dog dog1 = new Dog("멍멍이1", 2);
Dog dog2 = new Dog("멍멍이2", 5);
System.out.println("1. 단순 toString 호출");
System.out.println(car.toString());
System.out.println(dog1.toString());
System.out.println(dog2.toString());
System.out.println("=================================");
System.out.println("2. println 내부에서 toString 호출");
System.out.println(car);
System.out.println(dog1);
System.out.println(dog2);
System.out.println("=================================");
System.out.println("3. Object 다형성 활용");
ObjectPrinter.print(car);
ObjectPrinter.print(dog1);
ObjectPrinter.print(dog2);
}
}
실행 결과
1. 단순 toString 호출
langReview.object.tostring.Car@4e50df2e
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}
=================================
2. println 내부에서 toString 호출
langReview.object.tostring.Car@4e50df2e
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}
=================================
3. Object 다형성 활용
객체 정보 출력: langReview.object.tostring.Car@4e50df2e
객체 정보 출력: Dog{dogName='멍멍이1', age=2}
객체 정보 출력: Dog{dogName='멍멍이2', age=5}
'Car' 인스턴스는 'toString()' 을 재정의 하지 않았습니다.
따라서 'Object' 가 제공하는 기본 'toString()' 메서드를 사용합니다.
'Dog' 인스턴스는 'toString()' 을 재정의 한 덕분에 객체의 상태를 명확하게 확인할 수 있습니다.
1️⃣ ObjectPrinter.print(Object obj) 분석 - Car 인스턴스
ObjectPrinter.print(car);
void print(Object obj = car(Car)) { // 인수 전달
String string = "객체 정보 출력: " + obj.toString();
}
'Object obj' 의 인수로 'car(Car)' 가 전달됩니다.
메서드 내부에서 'obj.toString()' 을 호출합니다.
'obj' 는 'Object' 타입입니다.
따라서 'Object' 에 있는 'toString()' 을 찾습니다.
이때 자식에 재정의(Overriding, 오버라이딩)된 메서드가 있는지 찾아봅니다.
재정의된 메서드가 없을 경우에는 'Object.toString()' 을 실행합니다.
2️⃣ ObjectPrinter.print(Object obj) 분석 - Dog 인스턴스
ObjectPrinter.print(dog); // main에서 호출
void print(Object obj = car(Car)) { // 인수 전달
String string = "객체 정보 출력: " + obj.toString();
}
'Object obj' 의 인수로 'dog(Dog)' 가 전달됩니다.
메서드 내부에서 'obj.toString()' 을 호출합니다.
'obj' 는 'Object' 타입입니다.
따라서 'Object' 에 있는 'toString()' 을 찾습니다.
이때 자식에 재정의(Overriding, 오버라이딩)된 메서드가 있는지 찾아봅니다.
'Dog' 에 재정의된 메서드가 있습니다.
'Dog.toString()' 을 실행합니다.
🙋♂️ 참고 - 객체의 참조값 직접 출력
'toString()' 은 기본으로 객체의 참조값을 출력합니다.
그런데 'toString()' 이나 'hashCode()' 를 재정의하면 객체의 참조값을 출력할 수 없습니다.
이때는 다음 코드를 사용하면 객체의 참조값을 출력할 수 있습니다.
String refValue = Integer.toHexString(System.identityHashCode(dog1));
System.out.println("refValue = " + refValue);
실행 결과
refValue = 30dae81
-
-
💾 [CS] 싱글톤 패턴
💾 [CS] 싱글톤 패턴.
1️⃣ 싱글톤 패턴(Singleton pattern)
싱글톤 패턴(singleton pattern)은 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴입니다.
하나의 클래스를 기반으로 여러 개의 개별적인 인스턴스를 만들 수 있지만, 그렇게 하지 않고 하나의 클래스를 기반으로 단 하나의 인스턴스를 만들어 이를 기반으로 로직을 만드는데 쓰입니다.
보통 데이터베이스 연결 모듈에 많이 사용합니다.
하나의 인스턴스를 만들어 놓고 해당 인스턴스를 다른 모듈들이 공유하며 사용하기 때문에 인스턴스를 생성할 때 드는 비용이 줄어드는 장점이 있습니다.
하지만 의존성이 높아진다는 단점이 있습니다.
2️⃣ Java에서의 싱글톤 패턴.
Java에서 Singleton 패턴을 구현하는 방법은 여러 가지가 있지만, 가장 일반적으로 사용되는 방법 중 몇 가지를 소개하겠습니다.
Eager Initialization(즉시 초기화)
Lazy Initialization(지연 초기화)
Thread-safe Singleton(스레드 안전 싱글톤)
Synchronized Method
Double-checked Locking
Bill Pugh Singleton(Holder 방식)
1️⃣ Eager Initialization(즉시 초기화)
가장 간단한 방법으로, 클래스가 로드될 때 즉시 Singleton 인스턴스를 생성합니다.
public class Singleton {
// 유일한 인스턴스 생성
private static final Singleton instance = new Singleton();
// private 생성자: 외부에서 인스턴스 생성을 방지
private Singleton() {}
// 인스턴스를 반환하는 메서드
public static Singleton getInstance() {
return instance;
}
}
이 방법은 간단하고 직관적이지만, 클래스가 로드될 때 바로 인스턴스가 생성되기 때문에, 인스턴스가 사용되지 않더라도 메모리를 차지하게 됩니다.
2️⃣ Lazy Initialization(지연 초기화)
인스턴스가 처음으로 필요할 때 생성되도록 합니다.
이 방법은 초기화에 드는 비용이 큰 경우 유리합니다.
```java
public class Singleton {
// 유일한 인스턴스를 저장할 변수 (초기에는 null)
private static Singleton instance;
// private 생성자: 외부에서 인스턴스 생성을 방지
private Singleton() {}
// 인스턴스를 반환하는 메서드 (필요할 때만 생성)
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
```
이 방법은 다중 스레드 환경에서 안전하지 않기 때문에, 추가적인 동기화가 필요합니다.
3️⃣ Thread-safe Singleton(스레드 안전 싱글톤)
다중 스레드 환경에서 안전하게 Lazy Initialization을 구현하려면 동기화를 사용합니다.
1️⃣ Synchronized Method
public class Singleton {
private static Singleton instance;
private Singleton() {}
// synchronized 키워드로 스레드 안전하게 만듦
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
이 방법은 안전하지만, 성능에 약간의 영향을 줄 수 있습니다.
'synchronized' 로 인해 여러 스레드가 동시에 ‘getInstance()‘ 를 호출할 때 병목 현상이 발생할 수 있습니다.
2️⃣ Double-checked Locking
이 방법은 성능과 스레드 안전성을 모두 고려한 최적화된 방식입니다.
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
여기서 'volatile' 키워드는 인스턴스 변수가 스레드 간에 올바르게 초기화되도록 보장합니다.
4️⃣ Bill Pugh Singleton(Holder 방식)
이 방법은 Lazy Initialization을 사용하면서도, 성능과 스레드 안전성을 모두 보장합니다.
public class Singleton {
private Singleton() {}
// SingletonHolder가 클래스 로드 시점에 초기화됨
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
이 방법은 내부 정적 클래스가 JVM에 의해 클래스 로드 시 초기화되므로, 가장 권장되는 방식 중 하나입니다.
클래스가 로드될 때 초기화가 이루어지므로, 동기화나 추가적인 코드 없이도 스레드 안전성을 보장할 수 있습니다.
3️⃣ Spring Boot와 MySQL 데이터베이스의 연결 그리고 싱글턴 패턴.
Spring Boot에서 MySQL 데이터베이스를 연결할 때, 내부적으로 ‘싱글턴 패턴’ 이 사용됩니다.
그러나 이 패턴을 직접 구현할 필요는 없습니다.
‘Spring Framework’ 자체가 싱글턴 패턴을 활용하여 데이터베이스 연결 및 관리와 관련된 ‘Bean(객체)’ 을 관리합니다.
1️⃣ Spring Boot와 싱글턴 패턴.
Spring Framework는 기본적으로 각 Bean을 싱글턴 스코프로 관리합니다.
이는 특정 클래스의 인스턴스가 애플리케이션 컨텍스트 내에서 한 번만 생성되어 애플리케이션 전반에서 공유됨을 의미합니다.
2️⃣ 데이터베이스 연결에서의 싱글턴 패턴 사용.
DataSource Bean.
Spring Boot에서 MySQL과 같은 데이터베이스에 연결할 때 'DataSource' 라는 'Bean' 을 생성하여 관리합니다.
이 'DataSource' 객체는 데이터베이스 연결을 관리하는 역할을 하며, Spring은 이 'Bean' 을 싱글턴으로 생성하고 관리합니다.
즉, Spring 애플리케이션 내에서는 'DataSource' 객체가 하나만 생성되어 모든 데이터베이스 연결 요청에서 재사용됩니다.
EntityManagerFactory 및 SessionFactory.
JPA나 Hibernate와 같은 ORM을 사용하는 경우, 'EntityManagerFactory' 나 'SessionFactory' 와 같은 객체도 싱글턴 패턴에 의해 관리됩니다.
이들 객체는 데이터베이스 연결을 처리하고 트랜잭션을 관리하며, 역시 Spring에 의해 싱글턴으로 관리됩니다.
Spring의 싱글턴 관리.
Spring은 개발자가 'Bean' 을 직접 싱글턴으로 관리할 필요가 없도록, 애플리케이션의 컨텍스트 내에서 'Bean' 을 싱글턴으로 관리합니다.
데이터베이스와의 연결 관련 클래스들이 이 'Bean' 들로 구성되며, 이는 데이터베이스 연결이 효율적이고 일관되게 관리되도록 보장합니다.
3️⃣ 예시: Spring Boot에서 MySQL 연결 설정.
Spring Boot에서 MySQL 데이터베이스를 연결하기 위한 일반적인 설정은 'application.properties' 파일이나 'application.yml' 파일에 데이터베이스 연결 정보를 추가하는 것입니다.
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
이 설정은 Spring Boot가 'DataSource' 'Bean' 을 자동으로 생성하도록 하며, 이 'Bean' 은 애플리케이션 내에서 싱글턴으로 관리됩니다.
4️⃣ ✏️ 요약
Spring Boot에서 MySQL과 같은 데이터베이스를 연결할 때, Spring은 내부적으로 싱글턴 패턴을 사용하여 데이터베이스 연결을 관리합니다.
'DataSource', 'EntityManagerFactory' 등의 객체가 싱글턴으로 관리되며, 이를 통해 애플리케이션 전반에 걸쳐 일관되고 효율적인 데이터베이스 연결 관리가 이루어집니다.
Spring 자체가 이 패턴을 처리하므로, 개발자는 별도로 싱글턴 패턴을 구현할 필요가 없습니다.
4️⃣ Java Servlet 컨테이너와 MySQL 데이터베이스 연결 그리고 싱글턴 패턴.
Java Servlet 컨테이너에서 MySQL 데이터베이스를 연결할 때, 싱글턴 패턴이 일반적으로 사용됩니다.
다만, 이 패턴은 애플리케이션 코드에서 직접 구현되는 것이 아니라, 서블릿 컨테이너나 데이터베이스 연결 관리 라이브러리에서 사용됩니다.
1️⃣ JDBC DataSource
서블릿 컨테이너(예: Tomcat, Jetty)에서 데이터베이스 연결을 설정할 때 보통 'DataSource' 를 사용합니다.
이 'DataSource' 객체는 보통 싱글턴으로 관리되며, 데이터베이스 연결 풀을 제공합니다.
Connection Pooling
서블릿 컨테이너는 데이터베이스 연결을 관리하기 위해 연결 풀링(Connection pooling)을 사용합니다.
연결 풀은 여러 데이터베이스 연결을 미리 생성하고 재사용하도록 관리합니다.
연결 풀을 관리하는 객채는 'DataSource' 이고, 이는 애플리케이션 내에서 싱글턴으로 관리되어, 여러 서블릿에서 동일한 'DataSource' 객체를 사용하여 효율적으로 데이터베이스에 연결할 수 있습니다.
2️⃣ 싱글턴 패턴의 활용.
DataSource 객체
서블릿 컨테이너는 보통 'DataSource' 객체를 싱글턴으로 관리합니다.
'DataSource' 는 데이터베이스 연결 풀을 관리하며, 이 객체가 한 번만 생성되어 애플리케이션 전반에 걸쳐 재사용됩니다.
Connection 객체
각 요청마다 데이터베이스 연결이 필요할 때마다 새로운 'Connection' 객체가 생성되거나 풀에서 가져오게 됩니다.
하지만 'DataSource' 자체는 싱글턴으로 관리되기 때문에, 동일한 'DataSource' 객체를 통해 연결이 이루어집니다.
3️⃣ 예시: Tomcat에서 DataSource 설정
Tomcat과 같은 서블릿 컨테이너에서 MySQL 데이터베이스와의 연결을 설정하는 일반적인 방법은 'context.xml' 파일에서 'DataSource' 를 정의하는 것입니다.
<Context>
<Resource name="jdbc/MyDB"
auth="Container"
type="javax.sql.DataSource"
maxTotal="100"
maxIdel="30"
maxWaitMillis="10000"
username="root"
password="password"
driverClassName="com.myslq.cj.jdbc.Driver"
url="jdbc:mysql://localhost:3306/mydb"/>
</Context>
이 설정은 'jdbc/MyDB' 라는 JNDI 리소스를 정의하고, 'DataSource' 객체를 생성하여 연결 풀링을 관리합니다.
이 'DataSource' 는 Tomcat 내에서 싱글톤으로 관리됩니다.
4️⃣ 싱글턴 패턴의 이점.
효율성.
여러 서블릿이 동일한 'DataSource' 객체를 공유함으로써 메모리와 자원을 절약할 수 있습니다.
관리의 용이성.
데이터베이스 연결 관리를 중앙화할 수 있으며, 코드에서 직접 관리할 필요 없이 서블릿 컨테이너가 이를 담당합니다.
5️⃣ ✏️ 요약
Java Servlet 컨테이너에서 MySQL 데이터베이스를 연결할 때, 싱글턴 패턴은 주로 DataSource 객체에 적용됩니다.
이 DataSource 객체는 서블릿 컨테이너에 의해 싱글턴으로 관리되며, 데이터베이스 연결 풀을 통해 효율적으로 데이터베이스 연결을 처리합니다.
이를 통해 애플리케이션 전반에 걸쳐 일관되고 성능이 최적화된 데이터베이스 연결 관리가 이루어집니다.
3️⃣ Java 애플리케이션 서버와 MySQL 데이터베이스의 연결 그리고 싱글턴 패턴.
Java 애플리케이션 서버에서 MySQL 데이터베이스를 연결할 때, 싱글턴 패턴은 우요한 역할을 합니다.
그러나 이 패넡은 애플리케이션 코드에서 직접 구현되지 않으며, 애플리케이션 서버나 데이터베이스 연결 관리 라이브러리에서 사용됩니다.
1️⃣ DataSource와 Connection Pooling
Java 애플리게이션 서버(예: JBoss/WildFly, GlassFish, WebSphere)에서 데이터베이스를 연결할 때 일반적으로 'JDBC DataSource' 와 'Connection Pooling' 을 사용합니다.
이때 DataSource 객체는 싱글턴으로 관리되며, 데이터베이스 연결의 효율성을 높이기 위해 연결 풀을 사용합니다.
DataSource 싱글턴 관리
애플리케이션 서버는 데이터베이스와의 연결을 관리하기 위해 DataSource를 생성합니다.
이 DataSource 객체는 서버에서 싱글턴으로 관리됩니다.
즉, 애플리케이션 전반에 걸쳐 동일한 DataSource 객체가 사용됩니다.
DataSource는 내부적으로 데이터베이스 연결 풀을 관리하며, 여러 클라이언트 요청에서 동일한 데이터베이스 연결 객체를 재사용합니다.
Connection 객체 관리
데이터베이스와의 실제 연결을 관리하는 Connection 객체는 매번 새로운 요청이 있을 때마다 DataSource에서 가져오지만, DataSource는 싱글턴으로 관리되므로 전체 애플리케이션에서 일관된 연결 풀이 사용됩니다.
2️⃣ Java EE 환경에서의 DataSource 관리
Java EE 애플리케이션 서버에서는 'JNDI(Java Naming and Directory Interface)' 를 통해 DataSource를 관리합니다.
이는 서버의 전역 설정에서 관리되며, 여러 애플리케이션이 동일한 데이터베이스 연결을 공유할 수 있도록 합니다.
JNDI를 통한 DataSource 설정 예시
```xml
- 이 설정은 애플리케이션 서버가 싱글턴 DataSource 객체를 생성하고 관리하도록 합니다.
### 3️⃣ 싱글턴 패턴의 역할.
- **효율성**
- 싱글턴으로 관리되는 DataSource는 애플리케이션 서버 전체에서 하나의 객체로 유지되며, 이를 통해 메모리와 자원 사용이 최적화됩니다.
- **일관성**
- 동일한 데이터베이스 연결 풀을 사용하기 때문에 애플리케이션 전방에 걸쳐 데이터베이스 연결이 일관되게 관리됩니다.
- **관리 용이성**
- 데이터베이스 연결 관리가 중앙화되어, 각 애플리게이션에서 따로 관리할 필요 없이 서버에서 통합 관리됩니다.
### 4️⃣ EJB와의 통합.
- JavaEE 환경에서 EJB(Enterprise JavaBeans)는 주로 애플리케이션 서버에서 관리되는 비즈니스 로직을 구현하는 데 사용됩니다.
- EJB에서 데이터베이스 연결을 사용할 때도 싱글턴 패턴이 적용된 DataSource를 통해 연결이 이루어집니다.
```java
@Stateless
public class MyService {
@Resource(lookup = "java:/jdbc/MyDB")
private DataSource dataSource;
public void doSomething() {
try (Connection connection = dataSource.getConnection()) {
// 데이터베이스 작업 수행
} catch (SQLException e) {
e.printStackTrace();
}
}
}
이 코드에서 'dataSource' 는 서버에 의해 관리되는 싱글턴 DataSource 객체를 참조하며, 이를 통해 데이터베이스 연결을 처리합니다.
5️⃣ ✏️ 요약,
Java 애플리케이션 서버에서 MySQL 데이터베이스를 연결할 때, 싱글턴 패턴은 DataSource와 같은 중요한 객체 관리에 사용됩니다.
이 패턴을 통해 애플리케이션 서버는 데이터베이스 연결을 효율적이고 일관되게 관리할 수 있으며, 연결 풀링을 통해 자원 사용을 최적화합니다.
애플리케이션 서버가 DataSource를 싱글턴으로 관리함으로써, 서버 전반에 일관된 데이터베이스 연결을 제공하고 효율성을 극대화할 수 있습니다.
-
🍃[Spring] slf4j와 logback.
🍃[Spring] slf4j와 logback.
1️⃣ slf4j
'SLF4J(Simple Logging Facade for Java)' 는 Java 애플리케이션에서 로그 기록을 쉽게 관리하고 다른 로깅 프레임워크와 통합할 수 있도록 도와주는 로깅 인터페이스입니다.
'SLF4J' 는 다양한 로깅 프레임워크(e.g, Log4j, Logback, java.util.logging 등)에 대해 공통된 인터페이스를 제공하여 개발자가 특정 로깅 프레임워크에 종속되지 않고 유연하게 로그를 관리할 수 있도록 합니다.
1️⃣ slf4j의 주요 기능.
로깅 프레임워크와의 추상화
slf4j는 여러 로깅 프레임워크에 종속되지 않게 합니다.
예를 들어, 코드에서 slf4j 인터페이스를 사용하면 나중에 로깅 프레임워크를 쉽게 교체할 수 있습니다.
로깅 성능 최적화
slf4j는 문자열 병합에 따른 성능 문제를 피할 수 있도록 지원합니다.
예를 들어, slf4j는 로그 메시지의 문자열 결합을 지연시켜, 로그가 실제로 기록될 때만 결합이 발생하도록 합니다.
API 일관성
slf4j를 사용하면 로깅을 위한 일관된 API를 제공받을 수 있으며, 이를 통해 로깅을 표준화할 수 있습니다.
2️⃣ 사용 방법.
slf4j를 사용하기 위해서는, 우선 slf4j 인터페이스와 이를 구현한 로깅 프레임워크(예: Logback)를 프로젝트에 포함시켜야 합니다.
코드는 일반적으로 아래와 같이 사용됩니다.
```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyClass {
// Logger 생성
private static final Logger logger = LoggerFactory.getLogger(MyClass.class);
public void doSomthing() {
// 로그 메시지 기록
logger.info("This is an info message");
logger.debug("This is a debug message");
} } ``` - 이 코드는 **`'slf4j'`** 를 이용해 로그를 기록하는 예로, 로깅 메시지는 설정된 로깅 프레임워크를 통해 출력됩니다.
✏️ 요약.
slf4j는 Java 애플리케이션에서 로깅 프레임워크 간의 추상화 레이어를 제공하며, 코드가 특정 로깅 프레임워크에 종속되지 않도록 합니다.
이를 통해 유연한 로깅 관리가 가능해집니다.
2️⃣ logback
'logback' 은 Java 애플리케이션에서 사용되는 고성능 로깅 프레임워크로, slf4j의 권장 구현체 중 하나입니다.
'logback' 은 slf4j를 통해 접근할 수 있으며, 뛰어난 성능과 유연한 설정, 다양한 기능을 제공하는 것이 특징입니다.
1️⃣ logback의 주요 구성 요소.
Logback Classic
slf4j와 직접 통합되는 logback의 핵심 모듈입니다.
'Logback Classic' 은 Java 애플리케이션에서 로깅 기능을 수행하며, 다양한 로그 레벨(INFO, DEBUG, WARN, ERROR 등)을 지원합니다.
Logback Core
Logback Classic과 Logback Access(웹 애플리케이션용)를 기반으로 하는 일반적인 로깅 기능을 제공합니다.
'Logback Core' 는 Appender, Layout, Filter 등과 같은 기본 구성 요소를 포함합니다.
Logback Access
웹 애플리케이션에서 HTTP 요청과 응답을 로깅할 수 있도록 지원하는 모듈입니다.
주로 Java Servlet 환경에서 사용됩니다.
3️⃣ logback의 특징.
높은 성능
'logback' 은 빠른 로깅 성능을 제공하며, 특히 대규모 애플리케이션에서 효과적입니다.
유연한 구성
'logback' 은 XML 또는 Groovy 스크립트로 로깅 설정을 구성할 수 있습니다.
이를 통해 다양한 조건에 따라 로깅 동작을 세밀하게 제어할 수 있습니다.
조건부 로깅
'logback' 은 특정 조건에서만 로깅을 수행하도록 설정할 수 있어, 불필요한 로그 기록을 줄이고 성능을 최적화할 수 있습니다.
이전 로그 프레임워크와의 호환성
'logback' 은 기존의 'Log4j' 설정 파일을 사용할 수 있는 기능을 제공하여, 기존 'Log4j' 사용자가 쉽게 'logback' 으로 전환할 수 있도록 돕습니다.
다양한 출력 형식
'logback' 은 콘솔, 파일, 원격 서버, 데이터베이스 등 다양한 출력 대상으로 로그를 기록할 수 있으며, 출력 형식을 자유롭게 정의할 수 있습니다.
4️⃣ logback 사용 예제.
<configuration>
<!-- 콘솔에 로그를 출력하는 Appender -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 파일에 로그를 기록하는 Appender -->
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>mylog.log</file>
<append>true</append>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 루트 로거 설정 -->
<root level="debug">
<appender-ref ref="STDOUT" />
<appender-red red="FILE" />
</root>
</configuration>
이 예시는 콘솔과 파일에 로그를 출력하도록 설정하는 간단한 예시입니다.
logback은 이외에도 복잡한 요구 사항을 충족할 수 있는 다양한 기능을 제공하고 있습니다.
✏️ 요약.
logback은 Java 애플리케이션에서 사용되는 고성능 로깅 프레임워크로, slf4j와 함께 사용됩니다.
logback은 유연한 설정과 높은 성능, 다양한 기능이 있습니다.
-
-
-
-
-
-
-
🌐[Network] 프로토콜과 인터페이스(Protocol and Interface)
🌐[Network] 프로토콜과 인터페이스(Protocol and Interface)
1️⃣ 프로토콜과 인터페이스(Protocol and Interface).
네트워크 사용자가 통신한다는 것은 데이터를 서로 주고받는다는 것을 의미합니다.
최종 사용자가 데이터를 보내고 받으려면 양쪽 호스트에서 실행되는 OSI 7계층의 모듈이 유기적으로 연동되어야 합니다.
즉, 호스트끼리 통신하는 과정에서는 각 계층의 모듈이 상대 호스트의 동일 계층과 개별적으로 논리적 통신을 수행해야 합니다.
예를 들어, 통신 양단의 한쪽 호스트의 계층 n 모듈은 상대 호스트의 계층 n 모듈과 통신합니다.
이와 같이 각각의 계층은 정해진 방식과 절차에 따라 상대 계층과 통신하는데, “이 과정에서 필요한 규칙을 프로토콜(Protocol)” 이라고 합니다.
“상하위의 계층 간에는 인터페이스(Interface)” 라는 규칙이 존재하고, “하위 계층이 상위 계층에 제공하는 인터페이스를 특별히 서비스(Service)” 라 부릅니다.
위 그림은 계층 n과 계층 n-1의 2개 모듈로 구성된 계층 모델에서 서로 다른 두 호스트의 통신을 지원하기 위한 모듈 간의 관계를 프로토콜, 서비스, 데이터 전송의 관점에서 설명합니다.
한 호스트를 기준으로 데이터 전송은 위아래 양방향으로 모두 가능하며, 두 호스트 사이에서는 좌우 양방향으로 모두 가능합니다.
다만, 좌우 간의 물리적인 데이터 전송은 반드시 가장 아래의 물리 계층을 통하여 이루어집니다.
호스트 1과 호스트 2의 계층 n 프로토콜이 서로 통신하려면 계층 n-1 프로토콜의 서비스가 필요합니다.
즉, 호스트 1의 계층 n이 호스트 2의 계층 n에 데이터를 전송하는 과정은 하위의 계층 n-1을 통해 이루어집니다.
먼저, 호스트 1의 계층 n-1에 전송할 데이터를 주어 호스트 2에 전송하도록 부탁합니다.
그러면 호스트 1의 계층 n-1은 다시 하위 계층의 도움을 받아 호스트 2의 계층 n-1에 데이터를 보냅니다.
마지막으로 호스트 2의 계층 n-1이 수신한 데이터를 계층 n에 올려줌으로써 계층 n 사이의 통신이 완료됩니다.
이 원리는 ISO 7계층 모델에서 7개 계층에 모두 적용되며, 상대 호스트에 물리적으로 데이터를 전송하는 것은 맨 아래의 물리 계층입니다.
물리 계층 위에 있는 계층 프로토콜들은 각자의 정해진 기능을 수행하면서 논리적인 통신을 하는 것입니다.
-
🌐[Network] 네트워크 세그먼트(Network Segment).
🌐 네트워크 세그먼트(Network Segment).
1️⃣ 세그먼트(Segment).
네트워크 세그먼트(Network Segment)를 알아보기 전에 세그먼트(Segment)가 어떤 뜻을 가지고 있는지 궁금했습니다.
TTA 정보통신용어사전에서는 다음과 같이 세그먼트를 정의하고 있었습니다.
서로 구분되는 기억 장치 의 연속된 한 영역.
어떤 프로그램이 너무 커서 한 번에 주기억 장치 에 올라올 수 없이 갈아넣기 기법을 사용하여 쪼개었을 때, 나뉜 각 부분을 가리키는 말.
세그먼테이션 방식의 가상 기억 장치 에서 사용되는 것으로, 페이징에서 페이지와 비슷하나 길이가 가변이고 기억 장치 의 어느 곳에서도 자리할 수 있는 기억 장소 영역을 가리키는 말.
한 세그먼트는 프로그램의 논리적인 한 구성 단위를 저장한다.
계층 모형의 데이터베이스 에서 여러 항목이 모여 레코드에 해당하는 단위.
2️⃣ 네트워크 세그먼트(Network Segment).
네트워크 세그먼트(Network Segment)는 네트워크 내에서 논리적 또는 물리적으로 분리된 하나의 부분 또는 구역을 의미합니다.
네트워크를 여러 세그먼트로 나누는 것은 네트워크의 성능을 최적화하고, 보안을 강화하며, 트래픽 관리를 효율적으로 ㅎ기 위한 일반적인 방법입니다.
1️⃣ 네트워크 세그먼트의 주요 개념.
1. 물리적 세그먼트
물리적 네트워크 세그먼트는 실제 네트워크 장비(스위치, 라우터 등)와 물리적 케이블을 통해 분리된 네트워크 구역입니다.
예를 들어, 한 사무실 내에서 여러 층에 있는 네트워크가 각기 다른 물리적 스위치에 연결되어 있다면, 이들 층은 물리적으로 서로 다른 네트워크 세그먼트로 간주될 수 있습니다.
2. 논리적 세그먼트
논리적 네트워크 세그먼트는 물리적 인프라와 무관하게 네트워크를 논리적으로 나누는 것을 의미합니다.
이는 주로 VLAN(가상 로컬 영역 네트워크) 이나 서브넷(Subnet) 을 사용하여 이루어집니다.
예를 들어, 동일한 물리적 네트워크 내에서 서로 다른 부서의 컴퓨터를 논리적으로 분리하여 독립된 네트워크 세그먼트로 만들 수 있습니다.
2️⃣ 네트워크 세그먼트의 주요 목적.
1. 보안 강화
네트워크 세그먼트는 네트워크를 여러 개의 작은 구역으로 나누어, 각 구역의 트래픽을 독립적으로 관리하고 보호할 수 있습니다.
이를 통해 민감한 데이터를 처리하는 부서나 서버를 외부와 분리하여 보안을 강화할 수 있습니다.
2. 성능 최적화
네트워크를 세그먼트로 나누면 네트워크의 트래픽이 특정 구역 내에서만 흐르게 하여, 트래픽 혼잡을 줄이고 전체 네트워크의 성능을 향상시킬 수 있습니다.
예를 들어, 대용량 파일 전송이 필요한 부서의 네트워크 세그먼트를 다른 부서와 분리함으로써, 다른 부서의 네트워크 성능에 영향을 주지 않도록 할 수 있습니다.
3. 트래픽 관리
네트워크 세그먼트를 통해 트래픽을 효과적으로 관리하고, 특정 세그먼트의 트래픽을 제거할 수 있습니다.
네트워크 관리자는 각 세그먼트에 대해 별도의 라우팅 규칙, 방화벽 규칙 등을 적용할 수 있습니다.
4. 내결함성 향상
네트워크가 세그먼트로 분리되어 있으면, 한 세그먼트에서 발생한 문제가 다른 세그먼트로 확산되지 않도록 방지할 수 있습니다.
예를 들어, 한 세그먼트에서 발생한 네트워크 장애가 전체 네트워크에 영향을 미치지 않게 할 수 있습니다.
3️⃣ 예시: 서브넷을 이용한 네트워크 세그먼트.
서브넷은 네트워크 세그먼트의 한 유형입니다.
예를 들어, ‘10.0.0.0/16’ IP 주소 블록을 가진 VPC에서 ‘10.0.1.0/24’ 와 ‘10.0.2.0/24’ 라는 두 개의 서브넷을 생성할 수 있습니다.
이 두 서브넷은 동일한 VPC 내에서 서로 독립된 네트워크 세그먼트를 구성합니다.
이로 인해 한 서브넷에서 발생한 네트워크 트래픽은 기본적으로 다른 서브넷에 영향을 미치지 않습니다.
✏️ 요약
요약하자면, 네트워크 세그먼트는 네트워크를 보다 안전하고 효율적으로 운영하기 위해 분리된 네트워크 구역을 의미하며, 물리적 또는 논리적으로 구성될 수 있습니다.
-
-
-
-
☁️[AWS] 테넌시(Tenancy)
☁️[AWS] 테넌시(Tenancy).
“테넌시(Tenancy)” 라는 용어는 원래 부동산에서 사용되던 개념으로, 특정 공간을 임차하거나 소유하는 상태를 의미합니다.
“이 개념이 IT와 클라우드 컴퓨팅으로 확장되면서, 테넌시는 리소스나 환경을 특정 사용자나 조직이 사용하거나 소유하는 방식으로 사용됩니다.”
1️⃣ 테넌시의 일반적인 개념.
1️⃣ 부동산에서의 테넌시.
정의
테넌시는 주로 임차인이 집이나 건물을 사용하는 권리를 가지고 있는 상태를 의미합니다.
테넌시는 임대 계약을 통해 특정 기간 동안 특정 부동산을 사용하는 권리를 확보하게 됩니다.
유형
테넌시는 단독 테넌시(한 사람이 독점으로 사용)와 공동 테넌시(여러 사람이 함께 사용) 등으로 구분될 수 있습니다.
2️⃣ IT와 클라우드 컴퓨팅에서의 테넌시.
정의
IT 분야에서 테넌시는 특정 사용자나 조직이 IT 자원(서버, 네트워크, 데이터베이스 등)을 사용하는 상태를 의미합니다.
이 용어는 특히 클라우드 컴퓨팅에서 자주 사용되며, 리소스가 물리적 또는 논리적으로 어떻게 격리되고 공유되는지를 설명하는 데 사용됩니다.
유형
클라우드 컴퓨팅에서 테넌시는 주로 다음 두 가지 유형으로 구분됩니다.
단일 테넌시(Single-Tenancy)
한 명의 사용자나 조직이 독점적으로 자원을 사용하는 환경. 물리적 하드웨어 또는 소프트웨어 환경이 다른 사용자와 공유되지 않습니다.
예를 들어, 전용 서버 또는 전용 호스트 환경이 해당됩니다.
다중 테넌시(Multi-Tenancy)
여러 사용자나 조직이 동일한 물리적 하드웨어 또는 소프트웨어 환경을 공유하는 환경. 하지만 각 사용자의 데이터와 리소스는 논리적으로 격리되어 있습니다.
대부분의 퍼블릭 클라우드 서비스는 다중 테넌시 구조를 채택하고 있습니다.
3️⃣ 테넌시의 중요성.
자원 격리
테넌시는 사용자가 특정 IT 자원을 다른 사용자와 공유할지, 아니면 독립적으로 사용할지를 결정하는 중요한 요소입니다.
이를 통해 보안 수준을 강화하거나 비용을 절감할 수 있습니다.
보안 및 규정 준수
일부 조직은 규제 요구 사항을 충족하기 위해 단일 테넌시를 요구할 수 있습니다.
이 경우 물리적 자원을 다른 조직과 공유하지 않음으로써 보안성을 높일 수 있습니다.
비용 효율성
다중 테넌시는 자원을 공유함으로써 비용을 절감할 수 있습니다.
클라우드 서비스 제공자는 다중 테넌시를 통해 자원을 최적화하고, 이를 통해 사용자가 더 저렴한 비용으로 서비스를 이용할 수 있도록 합니다.
🎯 요약
“테넌시(Tenancy)” 는 특정 자원을 사용자 또는 조직이 사용하는 방식과 관련된 개념입니다.
부동산에서는 임차 관계를 의미합니다.
IT와 클라우드 컴퓨팅에서는 리소스가 어떻게 배치되고 격리되는지를 설명하는 용어로 사용됩니다.
클라우드 환경에서 테넌시의 선택은 보안, 성능, 비용에 큰 영향을 미칩니다.
-
☁️[AWS] 서브넷?
☁️[AWS] 서브넷(Subnet).
AWS애서 서브넷(Subnet)은 VPC(Virtual Private Cloud) 내에서 네트워크를 세분화하고 리소스를 논리적으로 분리하는 방법입니다.
서브넷은 VPC 내의 특정한 IP 주소 범위를 갖는 네트워크 세그먼트를 나타내며, AWS에서 네트워크 인프라를 구축하고 관리하는 데 중요하는 역할을 합니다.
1️⃣ VPC와 서브넷의 관계.
VPC(Virtual Private Cloud)
AWS에서 제공하는 가상 네트워크로, 사용자가 자신의 클라우드 리소스를 배치하고 관리할 수 있는 격리된 네트워크 환경입니다.
VPC는 사용자가 직접 IP 주소 범위, 서브넷, 라우팅 테이블, 네트워크 게이트웨이 등을 설정할 수 있도록 합니다.
서브넷(Subnet)
서브넷은 VPC 내에서 특정 IP 주소 범위를 갖는 작은 네트워크 세그먼트입니다.
“각 서브넷은 VPC의 전체 IP 주소 범위 내에서 특정한 부분을 할당받아 사용합니다.”
2️⃣ 서브넷의 유형.
퍼블릭 서브넷(Public Subnet)
인터넷 게이트웨이(Internet Gateway)에 연결된 서브넷으로, 이 서브넷에 배치된 리소스(예: EC2 인스턴스)는 퍼블릭 IP 주소를 가지며 인터넷과 직접 통시할 수 있습니다.
웹 서버와 같은 인터넷과 직접 연결이 필요한 리소스를 퍼블릭 서브넷에 배치합니다.
프라이빗 서브넷(Private Subnet)
인터넷 게이트웨이에 연결되지 않은 서브넷으로, 이 서브넷에 배치된 리소스는 퍼블릭 IP 주소가 없으며, 직접적으로 인터넷과 통신할 수 없습니다.
데이터베이스 서버와 같이 인터넷과 직접 연결될 필요가 없는 리소스를 프라이빗 서브넷에 배치합니다.
인터넷에 접속해야 할 경우, NAT 게이트웨이 또는 NAT 인스턴스 를 통해 간접적으로 인터넷에 접근할 수 있습니다.
3️⃣ 서브넷의 가용 영역(Availability Zone).
각 서브넷은 특정 가용 영역(AZ)에 속합니다.
이는 가용 영역마다 독립적인 네트워크 세그먼트를 구성할 수 있게 해줍니다.
예를 들어, VPC 내에 여러 가용 영역이 있을 때, 각각의 가용 영역에 서브넷을 만들어 고가용성 아키텍처를 구축할 수 있습니다.
4️⃣ 라우팅 테이블과 서브넷.
각 서브넷은 하나의 라우팅 테이블(Routing Table)과 연결됩니다.
라우팅 테이블은 네트워크 트래픽이 어떻게 라우팅되는지를 결정합니다.
예를 들어, 퍼블릭 서브넷은 라우팅 테이블에 인터넷 게이트웨이로 가는 경로가 설정되어 있지만, 프라이빗 서브넷은 그런 경로가 없습니다.
5️⃣ 서브넷의 보안 그룹 및 네트워크 ACL.
보안 그룹(Security Groups)
서브넷에 배치된 리소스(예: EC2 인스턴스)에 대한 인바운드 및 아웃바운드 트래픽을 제어하는 가상 방화벽입니다.
보안 그룹은 상태 기반으로 동작하며, 인스턴스 수준에서 적용됩니다.
네트워크 ACL(Network Access Control List)
서브넷 수준에서 적용되는 보안 레이어로, 서브넷 내의 모든 리소스에 대한 트래픽을 제어합니다.
네트웨크 ACL은 상태 비저장(State-less)으로, 각각의 요청과 응답을 별도로 처리합니다.
6️⃣ 서브넷의 사용 사례.
웹 서버 및 애플리케이션 서버 배치
퍼블릭 서브넷에 배치하여 외부에서 접근 가능한 웹 서버와 애플리케이션 서버를 운영합니다.
데이터베이스 서버 및 내부 애플리케이션 배치
프라이빗 서브넷에 배치하여 외부 접근이 제한된 안전한 네트워크 환경에서 데이터베이스 서버나 비공개 애플리케이션을 운영합니다.
멀티 AZ 아키텍처
각 가용 영역에 서브넷을 만들어 장애가 발생해도 시스템이 지속적으로 운영될 수 있는 고가용성 아키텍처를 구축합니다.
7️⃣ 서브넷과 CIDR 블록.
각 서브넷은 CIDR(Classless Inter-Domain Routing) 블록으로 정의된 IP 주소 범위를 가집니다.
예를 들어 '10.0.1.0/24' 와 같은 형태의 CIDR 블록이 서브넷의 IP 주소 범위를 정의합니다.
이 CIDR 블록에 따라 서브넷 내에서 사용 가능한 IP 주소의 범위가 결정됩니다.
🎯 서브넷을 잘 설계하고 구성하는 것은 AWS에서 네트워크 인프라를 최적화하고 보안을 강화하는 데 매우 중요합니다.
-
-
-
-
-
-
-
-
🌐[Network] OSI 7계층 모델.
🌐[Network] OSI 7계층 모델.
다수의 시스템을 서로 연결해서 통신하려면 선행적으로 전체 시스템 구조를 표준화해야 합니다.
국제 표준화 단체인 ISO(International Standard Organization)에서는 OSI(Open Systems Interconnection) 7계층 모델을 제안하여, 네트워크에 연결된 시스템이 갖추어야 할 기본 구조와 기능을 상세히 정의하고 있습니다.
1️⃣ 계층 구조.
OSI 7계층 모델(OSI 7 Layer Model)에 따르면, 네트워크에 연결된 호스트들은 위 그림과 같이 7개 계층으로 모듈화된 전송 기능을 갖추어야 합니다.
일반 사용자는 OSI 7계층 맨 위에 있는 응용 계층을 통해 데이터의 송수신을 요청하며, 이 요청은 하위 계층에 순차적으로 전달되어 맨 아래에 있는 물리 계층을 통해 상대 호스트에 전송됩니다.
그리고 요청이 각 계층으로 하달되는 과정에서 송수신 호스트 사이의 라우터들이 중개 기능을 수행합니다.
일반적으로 라우터는 하위 3개 계층의 기능만 수행합니다.
데이터를 수신하는 호스트에서는 송신 호스트와는 반대 방향으로 처리가 이루어집니다.
즉, 물리 계층으로 들어온 데이터는 순차적인 상향 전달 과정을 거쳐 응용 계층으로 올라갑니다.
수신 호스트에서 처리가 완료된 결과를 회신할 때는 반대 과정을 순차적으로 밟아서 송신 호스트로 되돌아갑니다.
데이터를 송수신하는 최종 주체는 송수신 호스트 양쪽에 위치한 응용 계층이며, 하부 계층인 표현 계층은 응용 계층을 지원하기 위한 고유 기능을 수행합니다.
이와 같은 계층 구조의 원리는 모든 상하 계층에 대하여 상대적으로 적용되며, 각각의 계층들은 데이터 전송에 필요한 기능들을 나누어 처리합니다.
1️⃣ 계층별 기능.
OSI 7계층 모델의 각 계층은 독립적인 고유 기능을 수행하며, 하위 계층이 바로 위 계층에서 서비스를 제공하는 형식으로 동작합니다.
1️⃣ 물리 계층(Physical Layer).
네트워크에서 호스트들이 데이터를 전송하려면 반드시 물리적인 전송 매체로 연결되어 있어야 합니다.
물리 계층(Physical Layer)은 호스트를 전송 매체와 연결하기 위한 인터페이스 규칙과 전송 매체의 특성을 다루며, 크게 유선 매체와 무선 매체로 구분됩니다.
2️⃣ 데이터 링크 계층(Data Link Layer).
물리 계층으로 데이터를 전송하는 과정에서는 잡음(Noise)등과 같은 여러 외부 요인에 의하여 물리적인 오류가 발생할 수 있습니다.
데이터 링크 계층(Data Link Layer)은 물리 계층의 오류에 관한 오류 제어(Error Control) 기능을 수행하며, 이를 위해서는 오류의 발생 사실을 인지하는 기능과 오류 복구 기능이 필요합니다.
물리 계층은 물리적 전송 오류를 감지(Sense)하는 기능을 제공해 상위 계층인 데이터 링크 계층에서 오류를 인지할 수 있도록 해줍니다.
그렇지 않은 경우는 데이터 링크 계층 스스로 별도의 기능을 수행하여 오류를 인지해야 합니다.
대표적인 물리적 오류로는 데이터가 도착하지 못하는 데이터 분실과 내용이 깨져서 도착하는 데이터 변형이 있습니다.
일반적으로 컴퓨터 네트워크에서 오류 복구는 송신자가 원래의 데이터를 재전송(Retransmission)하는 방식으로 처리합니다.
3️⃣ 네트워크 계층(Network Layer)
송신 호스트가 전송한 데이터가 수신 호스트까지 안전하게 도착하려면 여러 개의 중개 시스템인 라우터(Router)를 거쳐야 합니다.
이 과정에서 데이터가 올바른 경로를 선택할 수 있도록 지원하는 계층이 네트워크 계층(Network Layer)입니다.
기본적으로 네트워크 내부 구조는 라우터들로 구성되고, 네트워크 바깥쪽에 연결되는 송수신 호스트 사이의 데이터 중개 기능을 수행합니다.
데이터 중개 과정에서 오류가 발생할 수 있으므로 네트워크 계층에도 오류 제어 기능이 필요합니다.
네트워크 부하가 증가하면 특정 지역에 혼잡(Congestion)이 발생할 수 있는데, 혼잡 제어(Congestion Control)도 데이터의 전송 경로와 관계되므로 네트워크 계층이 담당합니다.
4️⃣ 전송 계층(Transport Layer)
컴퓨터 네트워크에서 데이터를 교환하는 최종 주체는 호스트가 아니고, 호스트 내부에서 실행되는 응용 네트워크 프로세스입니다.
네트워크 계층은 송수신 호스트 사이의 전송을 지원하지만, 응용 프로세스까지 전달하는 기능은 없습니다.
전송 계층(Transport Layer)은 송신 프로세스와 수신 프로세스 간의 연결(Connection) 기능을 제공하기 때문에 프로세스 사이의 안전한 데이터 전송을 지원합니다.
전송 계층은 데이터가 전송되는 최종적인 경로상의 양 끝단 사이의 연결이 완성되는 계층입니다.
일반적으로 계층 4까지의 기능은 운영체제에서 시스템 콜(System Call) 형태로 상위 계층에 제공하며, 계층 5~7의 기능은 응용 프로그램으로 작성됩니다.
5️⃣ 세션 계층(Session Layer)
세션 계층(Session Layer)은 전송 계층에서 제공하는 연결의 개념과 유사한 세션 연결을 지원하지만, 이보다는 더 상위의 논리적 연결입니다.
즉, 응용 환경에서 사용자 간 대화(Dialog) 개념의 연결로 사용되기 때문에 전송 계층의 연결과 구분됩니다.
예를 즐어, 인터넷에서 파일 송수신 중에 연결이 끊기면 이는 전송 계층의 연결이 종료된 것입니다.
이후 전송 계층의 연결을 다시 설정하여 이전에 데이터 송수신이 멈춘 지점부터 이어서 전송하는 기능을 세션 계층이 지원합니다.
6️⃣ 표현 계층(Presentation Layer)
표현 계층(Presentation Layer)은 전송되는 데이터의 의미(Semantic)를 잃지 않도록 올바르게 표현(Syntax)하는 방법을 다룹니다.
즉, 정보를 교환하는 호스트들이 표준화된 방법으로 데이터를 인식할 수 있게 해줍니다.
또한, 데이터의 표현이라는 본래의 기능에 더해, 현재의 표현 계층은 압축과 암호화라는 기능도 중요하게 다루고 있습니다.
동영상과 같은 대용량의 멀티미디어 데이터를 압축(Compression)하면 전송 데이터의 양을 줄일 수 있습니다.
암호화는 네트워크 보안 기능의 하나이며, 외부의 침입자로부터 데이터를 안전하게 보호하는 기술입니다.
인터넷을 통한 개인 정보의 처리와 금융 상거래가 증가하면서 인터넷 보안의 중요성이 커지고 있습니다.
7️⃣ 응용 계층(Application Layer)
응용 계층(Application Layer)은 일반 사용자를 위한 다양한 네트워크 응용 서비스를 지원합니다.
단순히 정보 검색을 지원하던 시대를 지나서 오늘날 인터넷 환경은 인공지능과 결합하는 추세로 발전되고 있습니다.
그에 따라 특정 분야에 한정되지 않고, 사회 전반의 모든 영역으로 네트워크 서비스는 발전하고 있습니다.
-
🌐[Network] 시스템 기초 용어.
🌐[Network] 시스템 기초 용어.
위 그림과 같이 네트워크는 외형적으로 시스템과 전송 매체의 조합으로 구성됩니다.
데이터 통신을 위한 전송 매체는 전송 대역, 전송 속도, 전송 오류율과 같은 물리적인 특성이 주 관심사이므로 논리적인 기능은 비교적 단순합니다.
시스템은 전송 매체를 이용해 다양한 연동 형태로 구성할 수 있으므로 개념의 폭이 넓고 복잡합니다.
1️⃣ 시스템의 구분.
네트워크를 구성하는 시스템이 반드시 일반 컴퓨터처럼 복잡한 기능을 수행해야 하는 것은 아니지만, 데이터 전송 기능을 포함하여 일정 정도의 컴퓨팅 기능을 보유합니다.
네트워크 시스템은 수행 기능에 따라 다음과 같이 다양한 명칭으로 부를 수 있습니다.
노드, 라우터, 호스트, 클라이언트, 서버
1️⃣ 노드(Node)
노드(Node)는 컴퓨터 이론 분야에서 특정 시스템을 가리키는 가장 일반적인 용어로 사용됩니다.
인터넷에서도 상호 연결된 시스템을 표현할 수 있는 가장 포괄적 의미로 사용되므로 데이터를 주고받을 수 있는 모든 시스템을 통칭합니다.
노드는 인터넷 내부를 구성하는 라우터와 인터넷 바깥쪽에 연결되어 데이터를 주고받는 호스트로 구분됩니다.
2️⃣ 라우터(Router)
라우터(Router)는 인터넷 내부를 구성하며, 기본으로 데이터 전송 기능을 포함합니다.
라우터의 주요 역할은 데이터 중개 기능이며, 인터넷 바깥쪽에 연결된 호스트들 사이의 데이터 전송이 인터넷 내부에서 최적의 경로를 통하여 이루어지도록 합니다.
3️⃣ 호스트(Host)
호스트(Host)는 인터넷 바깥쪽에 연결되어 일반 사용자들의 네트워크 접속 창구 역할을 합니다.
일반적인 컴퓨팅 기능을 갖춘 호스트는 네트워크 응용 프로그램을 실행할 수 있고, 사용자는 이 프로그램을 이용하여 다양한 인터넷 서비스를 제공받습니다.
호스트는 로스트 사이에 제공되는 서비스를 기준으로 클라이언트와 서버로 나눌 수 있습니다.
4️⃣ 클라이언트(Client)와 서버(Server)
클라이언트(Client)는 임의의 인터넷 서비스를 이용하는 응용 프로그램이고, 서버(Server)는 서비스를 제공하는 응용 프로그램입니다.
클라이언트와 서버의 개념은 서비스 단위로 이루어지므로 임의의 호스트가 클라이언트나 서버로 고정되지 않습니다.
이용하는 서비스의 종류에 따라서 클라이언트가 될 수도 있고, 서버가 될 수도 있습니다.
그러므로 특정 서비스를 기준으로 상대적인 관점에서 클라이언트와 서버라는 용어를 사용합니다.
일반적으로 응용 프로그램 혹은 서비스 단위가 아닌 호스트 단위로도 클라이언트와 서버를 사용하기도 합니다.
즉, 다양한 서비스를 제공하는 목적으로 특화된 호스트의 경우 호스트 자체를 서버라 부르기도 합니다.
서버는 클라이언트보다 먼저 실행 상태가 되어 클라이언트의 요청에 대기해야 합니다.
그리고 영원히 종료하지 않으면서 클라이언트의 요청이 있을 때마다 서비스를 반복해서 제공합니다.
2️⃣ 클라이언트(Client)와 서버(Server)
위 그림은 임의의 응용 서비스를 기준으로 클라이언트와 서버의 상대적인 관계를 설명합니다.
FTP(File Transfer Protocol)는 원격 호스트끼리 파일 송수신 기능을 제공하는 서비스이고, 텔넷(Telnet)은 원격 호스트에 로그인하는 서비스를 제공합니다.
호스트 2는 FTP 서비스를 제공하고, 호스트 3은 텔넷 서비스를 제공합니다.
먼저, FTP 서비스를 살펴보면 호스트 1은 호스트 2에 FTP 서비스를 요청합니다.
따라서 FTP 서비스를 기준으로 하면 호스트 1이 클라이언트가 되고, 호스트 2는 서버가 됩니다.
반면, 텔넷 서비스는 호스트 2가 호스트 3에 서비스를 요청합니다.
텔넷 서비스를 기준으로 하면 호스트 2가 클라이언트이고, 호스트 3은 서버입니다.
따라서 호스트 2는 사용하는 응용 서비스의 종류에 따라 클라이언트가 되기도 하고 서버가 되기도 합니다.
결론적으로 클라이언트와 서버라는 용어는 서비스 이용의 상대적 위치에 따라 결정됨을 알 수 있습니다.
서버의 명칭을 특정 호스트에 전용으로 부여해서 사용할 수도 있습니다.
특히 다양한 서비스 기능을 제공하는 대형 시스템을 서버로 설정해 다수의 클라이언트가 접속해서 서비스를 이용하도록 할 수 있습니다.
그러나 기능적인 관점에서는 위 그림에서처럼 호스트에서 실행되는 응용 서비스별로 구분하는 것이 더 정확합니다.
인터넷에서 네트워크 서비스의 기능은 대부분 응용 프로그램으로 구현되므로 보통 클라이언트 프로세스, 서버 프로세스라는 호칭이 더 자연스러울 수 있습니다.
-
-
-
-
-
-
-
-
🌐[Network] 네트워크 기초 용어.
🌐[Network] 네트워크 기초 용어.
이미 수많은 사람이 익숙하게 사용하고 있는 인터넷(Internet)은 연구소, 기업, 학교 등의 소규모 조직에서 사용하기 시작한 작은 단위의 네트워크(Network)들을 서로 연결하면서 발전하였습니다.
그 과정에서 자연스럽게 연결 방식의 표준화를 요구하게 되었고, 오늘날 전 세계로 확산되어 거대한 인터넷으로 성장하였습니다.
네트워크를 이해하려면 시스템, 인터페이스, 전송 매체, 프로토콜, 네트워크, 인터넷과 같은 용어를 먼저 이해해야 합니다.
네트워크(Network)는 하드웨어적인 전송 매체(Transmission Media)를 매개로 서로 연결되어 데이터를 교환하는 시스템(System)의 모음이며, 시스템과 전송 매체의 연결 지점에 대한 규격이 인터페이스(Interface)입니다.
시스템이 데이터를 교환할 때는 소프트웨어적으로 동작하는 통신 규칙인 프로토콜(Protocol)이 필요합니다.
인터페이스와 프로토콜은 서로 다른 시스템을 상호 연동해 동작시키기 위함이니 반드시 연동 형식의 통일이 필요하고, 이를 표준화(Standardization)라 합니다.
위 그림은 여러 시스템이 전송 매체로 연결되어 네트워크를 구성한 예입니다.
시스템은 반드시 일반 컴퓨터일 필요는 없으며, 보통 컴퓨팅 기능을 보유한 네트워크 장비들을 의미합니다.
그림과 같은 네트워크의 가장 바깥쪽에 스마트폰을 포함한 일반 사용자들의 컴퓨터가 연결되어 데이터 교환 작업을 수행합니다.
시스템들은 물리적으로 공유하는 전송 매체에 의하여 서로 연결되지만, 시스템이 전송 매체를 통해 데이터를 교환하려면 반드시 표준화된 프로토콜을 사용해야 합니다.
우리가 알고 있는 인터넷은 IP(Internet Protocol)라는 네트워크 프로토콜이 핵심적인 역할을 하는 네트워크의 집합체입니다.
여기서 IP는 프로토콜의 의미가 포함된 약자이지만 보통 IP 프로토콜이라 부릅니다.
1️⃣ 시스템(System)
내부 규칙에 따라 자율적으로 동작하는 대상을 가리킵니다.
자동차, 커피 자판기, 컴퓨터, 마이크로프로세서, 하드디스크 등과 같은 물리적인 대상뿐 아니라, 신호등으로 교통을 제어하는 운영 시스템, Mac OS 등의 운영체제, 프로그램의 실행 상태를 의미하는 프로세스와 같은 소프트웨어적인 대상들도 시스템입니다.
🤩 TIP: 네트워크 환경에서 동작하는 임의의 시스템은 다른 시스템과 데이터를 교환하는 기능이 필수적입니다.
시스템의 동작에 필요한 외부 입력이 있을 수 있으며, 내부 정보와 외부 입력의 조합에 따른 출력(시스템 실행의 결과물)이 있을 수 있습니다.
한편, 작은 시스템이 여러 개 모여 더 큰 시스템을 구성할 수 있으므로 크기를 기준으로 시스템을 나누지는 않습니다.
우리가 알고 있는 인터넷은 수많은 소규모 네트워크들이 서로 연동되는 반복적인 과정을 거쳐서 형성된 거대 연합체의 네트워크를 의미합니다.
2️⃣ 인터페이스(Interface)
시스템과 시스템을 연결하기 위한 표준화된 접촉 지점을 의미하며, 하드웨어적인 관점과 소프트웨어적인 관점이 모두 존재합니다.
하드웨어적인 예로서, 컴퓨터 본체와 키보드를 연결하여 제대로 동작하게 하려면 키보드의 잭을 본체의 정해진 위치에 꽂아야 합니다.
이렇게 하려면 상호 간의 데이터 교환을 위한 RS-232C, USB 등과 같은 논리적인 규격뿐만 아니라, 잭의 크기와 모양 같은 물리적인 규격도 표준화되어야 합니다.
소프트웨어적인 예로서, 프로그래밍 언어에서 함수 설계자는 함수 이름과 매개변수를 표준화하여 정의해야 하고, 함수 사용자는 이 정의에 맞게 함수 이름과 인수를 지정하여 사용할 수 있습니다.
인터페이스를 논리적인 상하 구조의 개념으로 이해할 필요는 없지만, 양방향으로 데이터를 주고 받는 경우와 한쪽에서 다른 쪽의 단방향으로 데이터를 보내는 경우로 나눌 수 있습니다.
3️⃣ 전송 매체(Transmission Media)
시스템끼리 정해진 인터페이스를 연동해 데이터를 전달하려면 물리적인 전송 수단인 전송 매체(Transmission Media)가 반드시 있어야 합니다.
전송 매체는 사람의 눈으로 볼 수 있는 동축 케이블을 포함하여 소리를 전파하는 공기, 무선 신호 등 다양하게 존재합니다.
인터페이스는 시스템 간의 물리적인 연동을 위한 논리적인 규격이고 인터페이스로 정해진 규격은 전송 매체를 통해 물리적으로 구현되며, 시스템끼리 데이터 전송을 가능하게 합니다.
4️⃣ 프로토콜(Protocol)
논리적으로 상호 연동되는 시스템이 전송 매체를 통해 데이터를 교환할 때는 표준화된 대화 규칙을 따르는데, 이 규칙을 프로토콜(Protocol)이라 합니다.
일반적으로 프로토콜은 상하 관계가 아닌 동등한 위치에 있는 시스템 사이의 규칙이라는 측면이 강조되어 인터페이스와 구분이 됩니다.
인터페이스는 위 그림과 같이 두 시스템이 연동하기 위한 특정한 접촉 지점(Access Point)을 의미하는 경우가 많지만, 프로토콜과 비교하여 인용될 때는 상하 개념이 적용됩니다.
즉, 네트워크의 계층 모델 구조에서 인터페이스는 상하 계층 사이의 관계를 다루고, 프로토콜은 동등 계층 사이의 관계를 다룹니다.
일반적으로 프로토콜은 주고받는 데이터의 형식과 그 과정에서 발생하는 일련의 절차적 순서에 무게를 둡니다.
5️⃣ 네트워크(Network)
통신용 전송 매체로 연결된 여러 시스템이 프로토콜을 사용하여 데이터를 주고받을 때, 이들을 하나의 단위로 통칭하여 네트워크(Network)라 부릅니다.
일반적인 컴퓨터 네트워크에서는 물리적인 전송 매체로 연결된 컴퓨터들이 동일한 프로토콜을 이용해 서로 데이터를 주고 받습니다.
소규모 네트워크가 모여 더 큰 네트워크를 구성할 수 있는데, 네트워크끼리는 라우터(Router)라는 중개 장비를 사용해서 연결합니다.
6️⃣ 인터넷(Internet)
전 세계의 모든 네트워크가 유기적으로 연결되어 동작하는 통합 네트워크입니다.
인터넷에서 사용되는 시스템, 인터페이스, 전송 매체, 프로토콜들은 그 종류가 매우 복잡하고 다양하지만, 데이터 전달 기능에 한해서는 공통으로 IP(Internet Protocol) 프로토콜을 사용합니다.
즉, ISO의 OSI 7계층 모델에서 계층 3인 네트워크 계층의 기능을 IP 프로토콜이 수행하며 인터넷이라는 용어의 IP의 첫 단어인 Internet에서 유래했습니다.
7️⃣ 표준화(Standardization)
서로 다른 시스템이 상호 연동해 동작하려면 표준화(Standardization)라는 연동 형식의 통일이 필요합니다.
예를 들어, 프린트 용지를 생각해봅시다.
일반적으로 프린터와 프린트 용지를 만드는 회사는 다릅니다.
하지만 사전에 A4 규격이라는 통일된 틀을 만들어두었기 때문에 서로 다른 회사에서 생산한 프린터와 프린트 용지를 자유롭게 사용할 수 있습니다.
현대 산업사회가 눈부시게 성장한 배경에는 증기기관의 개발에 따른 에너지 동력원의 발전이 있었습니다.
지금은 인간의 노동력이라는 한계를 넘어 인공지능으로 대표되는 새로운 차원의 사회 발전 단계인 4차 산업혁명이 진행되고 있습니다.
그러나 이와 다른 관점에서 더 근원적인 발전 배경을 살펴보면, 표준화 원리를 바탕으로 한 레고의 조합 개념이 산업 전반에 존재해왔기 때문임을 알 수 있습니다.
-
-
-
☁️[AWS] 인바운드 규칙(Inbounds Rules)와 아웃바운드 규칙(Outbound Rules)
☁️[AWS] 인바운드 규칙(Inbounds Rules)와 아웃바운드 규칙(Outbound Rules).
AWS EC2 인스턴스의 보안 그룹은 인스턴스에 대한 네트워크 트래픽을 제어하는 가상 방화벽 역할을 합니다.
보안 그룹에는 인바운드(들어오는 트래픽) 및 아웃바운드(나가는 트래픽) 규칙이 있으며, 각 규칙은 특정 유형의 트래픽을 허용하거나 차단할 수 있습니다.
1️⃣ 인바운드 규칙(Inbound Rules)
인바운드 규칙은 외부에서 인스턴스로 들어오는 트래픽을 제어합니다.
이 규칙에 따라 특정 IP 주소나 IP 범위에서 오는 트래픽만 허용됩니다.
예시
SSH 접속을 허용하기 위해 포트 22번에서 들어오는 트래픽을 허용할 수 있습니다.
이 경우, 특정 IP 주소(예: 203.0.113.0/24)에서 SSH 접속이 가능하도록 설정할 수 있습니다.
웹 서버를 운영 중이라면 HHTP(포트 80) 또는 HTTPS(포트 443) 트래픽을 허용할 수 있습니다.
중요한 점
보안 그룹은 허용 규칙만 존재하며, 명시적으로 허용된 트래픽만 인스턴스로 들어올 수 있습니다.
기본적으로, 보안 그룹에 명시되지 않은 모든 인바운드 트래픽은 차단됩니다.
2️⃣ 아웃바운드 규칙(Outbound Rules)
아웃바운드 규칙은 인스턴스에서 외부로 나가는 트래픽을 제어합니다.
기본적으로 모든 아웃바운드 트래픽이 허용되지만, 필요에 따라 이를 제한할 수 있습니다.
예시
인스턴스가 특정 외부 서비스로의 연결을 허용하도록, 해당 서비스의 IP 주소나 포트로 나가는 트래픽을 허용할 수 있습니다.
만약 인스턴스가 외부로 데이터를 보내는 것을 제한하고자 한다면, 특정 포트나 IP 주소로의 나가는 트래픽을 차단할 수 있습니다.
중요한 점
기본적으로 아웃바운드 트래픽은 모두 허용되지만, 아웃바운드 규칙을 설정하여 특정 트래픽만 허용하도록 제한할 수 있습니다.
3️⃣ 보안 그룹 작동 방식
보안 그룹은 상태 정보를 가지고 있습니다. 즉, 인스턴스로 들어오는 요청이 허용되었다면, 그 요청에 대한 응답은 아웃바운드 규칙과 관계없이 허용됩니다.
보안 그룹은 AWS 계정 수준에서 관리되며, 여러 인스턴스에 동일한 보안 그룹을 적용할 수 있습니다.
보안 그룹의 변경 사항은 즉시 적용되므로, 보안 그룹을 수정하면 해당 인스턴스에 바로 반영됩니다.
🙋♂️ 마무리
보안 그룹을 올바르게 설정하는 것은 EC2 인스턴스를 안전하게 운영하기 위해 매우 중요합니다.
인바운드 규칙을 통해 접근을 제한하고, 필요에 따라 아웃바운드 규칙을 설정하여 인스턴스의 네트워크 트래픽을 제어할 수 있습니다.
-
-
☁️[AWS] 서비스 제공 형태에 따른 클라우드 분류.
☁️[AWS] 서비스 제공 형태에 따른 클라우드 분류.
클라우드 서비스는 제공하는 서비스에 따라 SasS, PaaS, IaaS 로 나눌 수 있습니다.
SaaS(Software as a Service)
응용 프로그램을 서비스로 제공하는 형태입니다.
많은 사람이 사용하는 Gmail, Dropbox, Office365, Zoom이 대표적인 SaaS 입니다.
PaaS(Platform as a Service), IaaS(Infrastructure as a Service)
응용 프로그램을 만들기 위한 기능을 서비스로 제공합니다.
이 서비스는 직접 응용 프로그램을 개발하는 사용자를 위한 서비스로, 사용자는 제공 받은 기능을 조합해 응용 프로그램을 개발합니다.
PaaS와 IaaS의 차이는 클라우드 서비스 제공자가 관리하는 범위입니다.
PaaS
클라우드 서비스 제공자는 OS 및 미들웨어까지 관리하고, 필수 기능만 사용자에게 제공합니다.
AWS에서 관리형 서비스로 제공하는 RDS나 DynamoDB, Lambda 등이 여기에 해당합니다.
유지보수는 AWS가 담당하며 사용자는 AWS에서 제공하는 범위 안에서 자유롭게 기능을 이용할 수 있습니다.
IaaS
서버 및 네트워크 기능만 제공하며 설정과 관리는 사용자의 몫입니다.
AWS의 EC2와 VPC, EBS와 같이 사용자가 자유롭게 설정할 수 있는 서비스가 IaaS에 해당합니다.
-
-
-
🌐[Network] CIDR이란?
🌐[Network] CIDR이란?
CIDR(Classless Inter-Domain Routing) 은 IP 주소와 관련된 라우팅 방법을 정의하는 표기법입니다.
CIDR 표기법은 IPv4 주소를 네트워크와 호스트 부분으로 나누고, 네트워크의 크기(서브넷 크기)를 정의하는 데 사용됩니다.
CIDR 표기법은 다음과 같은 형식으로 표현됩니다.
192.168.0.0/24
이 표기법은 두 부분으로 나뉩니다.
IP 주소 부분 : 192.168.0.0
서브넷 마스크 부분: /24
여기서 /24 는 서브넷 마스크의 길이를 나타내며, 이는 네트워크 부분의 비트 수를 의미합니다.
즉, 192.168.0.0/24 는 24비트가 네트워크를 정의하고 나머지 8비트(총 32비트 중)가 호스트를 정의하는 서브넷을 나타냅니다.
1️⃣ IPv4 CIDR의 구조.
IPv4 주소는 32비트로 구성되어 있으며, 이를 네 개의 8비트 옥텟으로 표현합니다.
예를 들어 다음과 같습니다.
11000000.10101000.00000000.00000000 (이진)
192.168.0.0 (십진)
CIDR 표기법에서 /24 는 첫 번째 24비트(세 개의 옥텟)가 네트워크 주소를 나타낸다는 것을 의미합니다.
이 경우 192.168.0.0 네트워크에는 192.168.0.1 에서 192.168.0.254 까지의 호스트 주소를 가질 수 있습니다.
2️⃣ IPv4 CIDR의 용도.
서브네팅 : 큰 네트워크를 작은 서브넷으로 나누기 위해 CIDR을 사용합니다.
라우팅 : 인터넷 서비스 제공자(ISP) 및 네트워크 관리자는 CIDR을 사용하여 라우팅 테이블을 관리하고, IP 주소 공간을 효율적으로 사용합니다.
IP 주소 관리 : CIDR은 IP 주소를 할당하고 네트워크를 관리하는 데 사용됩니다.
3️⃣ CIDR 블록의 생성 기준.
CIDR 블록을 생성할 때는 네트워크 크기와 필요한 IP 주소 수를 고려해야 합니다. 일반적인 기준은 다음과 같습니다.
1. 네트워크 크기 계산 :
/24 서브넷은 256개의 IP 주소(호스트)를 제공합니다. 이 중 두개의 주소(네트워크 주소와 브로드캐스트 주소)를 제외하고, 254개의 호스트 IP 주소를 사용할 수 있습니다.
/16 서브넷은 65,536개의 IP 주소를 사용할 수 있습니다.
/32 는 단일 IP 주소를 나타냅니다.
2. 필요한 IP 주소 수에 따라 결정 :
만약 50개의 장치를 연결해야 한다면, /26(64개 IP 주소 제공) 서브넷을 사용할 수 있습니다.
큰 네트워크에는 /16 이나 /12 처럼 더 작은 서브넷 마스크를 사용할 수 있습니다.
3. 보안 및 관리 :
더 작은 서브넷(CIDR 블록)을 사용하면 네트워크 트래픽을 보다 효율적으로 관리하고, 보안을 강화할 수 있습니다.
4️⃣ 예시.
/32 : 단일 IP 주소. 예: 192.168.0.1/32
/24 : 256개의 IP 주소 제공, 주로 작은 네트워크에서 사용. 예: 192.168.0.0/24
/16 : 65,536개의 IP 주소 제공, 더 큰 네트워크에 사용. 예: 192.168.0.0/16
/8 : 16,777,216개의 IP 주소 제공, 매우 큰 네트워크에서 사용. 예: 10.0.0.0/8
5️⃣ 결론.
CIDR 표기법은 IP 주소와 서브넷 마스크를 결합한 표준입니다.
네트워크의 크기와 IP 주소와 필요 수를 기준으로 CIDR 블록을 생성합니다.
CIDR을 사용하면 네트워크를 보다 효율적으로 관리하고 라우팅 테이블을 최적화할 수 있습니다.
🙋♂️ CIDR 블록을 설계할 때, 사용하려는 네트워크 규모와 IP 주소 요구 사항을 염두에 두고, 적절한 서브넷 마스크 길이를 선택하는 것이 중요합니다.
-
-
📝[Post] 정적 웹사이트와 동적 웹사이트.
🙋♂️ 정적 웹사이트와 동적 웹사이트.
정적 웹사이트와 동적 웹사이트는 웹페이지를 생성하고 제공하는 방식에서 큰 차이를 보입니다.
각각의 특징을 이해하면 어떤 상황에서 어떤 타입의 웹사이트를 사용해야 하는지 결정하는 데 도움이 됩니다.
1️⃣ 정적 웹사이트.
정적 웹사이트는 미리 만들어진 HTML 파일들을 그대로 웹 서버에서 사용자의 브라우저로 전송하여 보여주는 웹사이트입니다.
이 파일들은 서버에 미리 저장되어 있으며, 사용자의 요청에 따라 변하지 않고 그대로 제공됩니다.
👍 정적 웹사이트의 장점.
단순성과 속도.
복잡한 서버 측 처리 없이 바로 파일을 전송하기 때문에 로딩 시간이 빠릅니다.
호스팅 비용.
낮은 서버 자원 사용으로 인해 비용이 저렴합니다.
보안.
동적 콘텐츠를 처리하는 서버 측 스크립트가 없어 보안 리스크가 상대적으로 낮습니다.
👎 정적 웹사이트의 단점.
유연성 부족.
각 페이지를 수동으로 업데이트해야 하며, 대규모 사이트에서는 유지 관리가 어려울 수 있습니다.
사용자 상호작용 부족.
사용자 입력에 따라 내용이 바뀌지 않으므로, 폼 제출이나 검색과 같은 기능을 직접 구현하기 어렵습니다.
2️⃣ 정적 웹사이트의 예시.
1. 포트폴리오 웹사이트.
웹 개발자, 디자이너, 사진작가 등의 포트폴리오를 위한 웹사이트들은 주로 정적입니다.
이 웹사이트들은 작품을 보여주는 갤러리, 연락처 정보, 이력서 등의 고정된 내용을 포함합니다.
2. 기업 정보 페이지.
소규모 기업이나 스타트업이 회사 정보, 제품 설명, 연락처 정보 등을 제공하는 단순한 웹사이트를 운영할 때, 이는 종종 정적 웹사이트로 구성됩니다.
3. 이벤트 안내 페이지.
특정 이벤트의 일시, 장소, 등록 방법 등을 안내하는 웹페이지로, 주로 내용의 변경이 적고, 정보의 전달이 주 목적일 때 정적 웹사이트로 구현됩니다.
3️⃣ 동적 웹사이트.
동적 웹사이트는 서버 측 프로그래밍 언어를 사용하여 사용자의 요청에 따라 실시간으로 웹페이지를 생성하고 제공합니다.
데이터베이스와의 상호작용을 통해 컨텐츠를 동적으로 생성하고 사용자의 요청에 맞춰 개별적으로 내용을 조정할 수 있습니다.
👍 동적 웹사이트의 장점.
유연성.
사용자의 입력이나 상호작용에 따라 내용을 쉽게 변경할 수 있습니다.
기능성.
데이터베이스에 정보를 저장하고 검색하는 등의 복잡한 기능을 구현할 수 있습니다.
개인화.
사용자의 선호나 행동에 따라 개인화된 경험을 제공할 수 있습니다.
👎 동적 웹사이트의 단점.
비용과 복잡성.
서버 측 처리를 위한 추가적인 자원이 필요하며, 구현과 유지 관리가 복잡해질 수 있습니다.
보안 위험.
데이터베이스와 서버 측 스크립트를 사용함으로써 보안 취약점이 발생할 수 있습니다.
속도.
페이지를 실시간으로 생성하므로 처리 시간이 길어질 수 있습니다.
4️⃣ 동적 웹사이트의 예시.
1. 전자 상거래 플랫폼.
Amazon, eBay 등의 쇼핑 웹사이트는 사용자의 검색, 구매 이력, 상품의 재고 상태 등에 따라 실시간으로 정보를 업데이트하고 표시해야 합니다.
이런 기능은 동적 웹사이트 기술을 필요로 합니다.
2. 소셜 네트워킹 서비스.
Facebook, Twitter와 같은 소셜 미디어 플랫폼은 사용자의 상호 작용에 기반하여 내용이 계속 업데이트 되며, 이러한 동적 상호 작용을 지원합니다.
3. 온라인 교육 플랫폼.
Coursera, Udemy, Inflearn와 같은 교육 플랫폼은 사용자가 선택한 강좌에 따라 개인화된 학습 내용을 제공하고, 퀴즈 점수를 기록하며, 진행 상태를 추적합니다.
🙋♂️ 마무리
정적 웹사이트와 동적 웹사이트 선택은 프로젝트의 요구 사항, 예산, 기대하는 사용자 경험 등에 따라 달라집니다.
간단한 정보 제공 사이트의 경우 정적 웹사이트가 적합할 수 있고, 사용자 상호작용과 데이터 처리가 중요하 서비스는 동적 웹사이트가 더 적합할 수 있습니다.
이러한 예시들을 통해 정적 웹사이트가 주로 고정된 내용을 제공하는 반면, 동적 웹사이트는 사용자의 입력과 상호작용에 따라 콘텐츠가 변경되는 복잡한 기능을 필요로 함을 알 수 있습니다.
각각의 사례에서 요구하는 기능과 특성에 맞춰 웹사이트의 형태를 결정합니다.
-
-
-
☕️[Java] @RequiredArgsConstructor의 역할.
☕️[Java] @RequiredArgsConstructor 역할.
RequiredArgsConstructor 어노테이션은 Lombok 라이브러리에서 제공하는 기능 중 하나로, 클래스에 필수적인 생성자를 자동으로 생성하는 역할을 합니다.
이 어노테이션을 클래스에 적용하면, Lombok 이 그 클래스의 final 필드 또는 @NonNull 어노테이션이 붙은 필드를 인자로 받는 생성자를 자동으로 생성합니다.
1️⃣ @RequiredArgsConstructor의 주요 기능.
1. 자동 생성자 생성
클래스 내의 모든 final 필드와 @NonNull 어노테이션이 붙은 필드에 대한 생성자를 자동으로 생성합니다.
이 생성자는 이 필드들을 초기화하는 데 필요한 파라미터를 요구합니다.
2. 코드 간결화
수동으로 생성자를 작성하는 번거로움을 줄여줍니다.
특히 많은 필드를 가진 클래스에서 유용하게 사용될 수 있습니다.
3. 불변성 강화
final 필드를 사용함으로써 클래스의 불변성을 강화할 수 있습니다.
생성자를 통해 한 번 설정되면, 이 필드들의 값은 변경될 수 없습니다.
4. 의존성 주입 용이
Spring과 같은 프레임워크에서 생성자를 통한 의존성 주입을 사용할 때 유용합니다.
필요한 의존성을 생성자를 통해 주입받기 때문에, 컴포넌트 간의 결합도를 낮출 수 있습니다.
2️⃣ 사용 예시.
다음은 @RequiredArgsConstructor 어노테이션을 사용한 간단한 클래스 예제입니다.
import lombok.RequiredArgsConstructor;
import lombok.NonNull;
@RequiredArgsConstructor
public class UserData {
private final String username; // final 필드에 대한 생성자 파라미터 자동 포함.
@NonNull private String email; // @NonNull 필드에 대한 생성자 파라미터 자동 포함.
// 추가 메소드 등
}
위 코드에서 UserData 클래스에는 username 과 email 두 필드가 있으며, username 은 final 로 선언되어 수정할 수 없고, email 은 @NonNull 어노테이션이 붙어 null 값을 허용하지 않습니다.
Lombok은 이 두 필드를 초기화하는 생성자를 자동으로 생성합니다.
3️⃣ 주의 사항.
@RequiredArgsConstructor 는 필드가 많고, 특히 final 또는 @NonNull 필드가 있는 경우 유용합니다.
그러나 생성자를 통한 초기화가 필요하지 않은 필드에는 적용되지 않습니다.
Lombok을 사용하면 코드가 간결해지고 가독성이 향상되지만, 코드의 명시성이 다소 떨어질 수 있습니다.
따라서 Lombok 사용 시, 팀 내에서 Lombok에 대한 이해도가 충분한지 확인하는 것이 좋습니다.
Lombok 의 @RequiredArgsConstructor 는 반복적인 코드 작성을 줄여주고, 오류 가능성을 감소시키며, 더 깔끔하고 관리하기 쉬운 코드베이스를 유지하는 데 도움을 줄 수 있습니다.
-
-
☕️[Java] @Transactional의 역할과 의미.
☕️[Java] @Transactional의 역할과 의미.
@Transaction 어노테이션은 스프링 프레임워크에서 제공하는 선언적 트랜젝션 관리 기능을 활용하기 위해 사용됩니다.
이 어노테이션을 사용함으로써, 특정 메서드 또는 클래스 전체에 걸쳐 데이터베이스 트랜잭션의 경계를 설정할 수 있습니다.
트랜잭션은 일련의 연산들이 전부 성공적으로 완료되거나, 하나라도 실패할 경우 전체를 취소(롤백)하여 데이터의 일관성과 정합성을 보장하는 것을 목적으로 합니다.
1️⃣ @Transactional의 주요 기능과 특징.
1. 자동 롤백
@Transactional 이 적용된 메서드에서 런타임 예외(RuntimeException)가 발생하면, 그 트랜잭션에서 수행된 모든 변경이 자동으로 롤백됩니다.
이는 데이터의 일관성을 유지하는 데 필수적입니다.
2. 프로파게이션(Propagation)
트랜잭션의 전파 행위를 제어합니다.
예를 들어, 이미 진행 중인 트랜잭션이 있을 때 새로운 트랜잭션을 시작할 것인지, 아니면 기존 트랜잭션을 참여할 것인지 결정할 수 있습니다.
REQUIRED(기본값) : 이미 진행 중인 트랜잭션이 있다면 그 트랜잭션이 참여하고, 없다면 새로운 트랜잭션을 시작합니다.
REQUIRED_NEW : 항상 새로운 트랜잭션을 시작합니다. 이미 진행 중인 트랜잭션이 있다면 잠시 보류합니다.
3. 격리 수준(Isolation Level)
다른 트랜잭션이 데이터에 동시에 접근했을 때 발생할 수 있는 문제를 제어합니다.
예를 들어, READ_COMMITTEED, REPEATED_READ, SERIALIZABLE 등 다양한 격리 수준을 지정할 수 있습니다.
4. 읽기 전용(Read-Only)
트랜잭션을 읽기 전용으로 설정할 수 있어, 데이터 수정이 이루어지지 않는다는 것을 데이터베이스 최적화 엔진에 알려 성능을 향상시킬 수 있습니다.
5. 롤백 규칙(Rollback Rules)
특정 예외가 발생했을 때 롤백을 수행할지 아니면 커밋을 수행할지를 세밀하게 제어할 수 있습니다.
기본적으로 런타임 예외에서는 롤백을 수행하고, 체크 예외에서는 커밋을 수행합니다.
2️⃣ 사용 예제.
import org.springframework.transaction.annotation.Transactional;
import org.springframework.stereotype.Service;
@Service
public class TransactionalService {
@Transactional(readOnly = true)
public User getUser(Long id) {
return userRepository.findById(id);
}
@Transactional(rollbackFor = Exception.class)
public User updateUser(User user) {
return userRepository.save(user);
}
}
위 예시처럼, getUser 메서드는 데이터를 변경하지 않고 조회만 수행하기 때문에 readOnly = true 로 설정했습니다.
반면, updateUser 메서드는 데이터를 변경할 가능성이 있으므로, 모든 예외(Exception)가 발생할 경우 롤백하도록 설정했습니다.
@Transactional 을 사용함으로써 개발자는 복잡한 트랜잭션 관리 코드를 직접 작성하지 않고도, 스프링 프레임워크가 제공하는 선언적 방식을 통해 간단하게 트랜잭션을 관리할 수 있게 됩니다.
이는 애플리케이션의 데이터 처리 로직을 더욱 안정적이고 효율적으로 만듭니다.
-
☕️[Java] ObjectMapper 클래스, 직렬화와 역직렬화
☕️[Java] ObjectMapper 클래스, 직렬화와 역직렬화.
ObjectMapper 는 주로 JSON 데이터를 처리하기 위해 사용되는 Jackson 라이브러리의 핵심 클래스입니다.
이 클래스는 자바 객체와 JSON 형식 간의 직렬화(Serialization)와 역직렬화(Deserialization)를 수행합니다.
ObjectMapper 는 JSON 데이터를 자바 객체로 변환하거나 자바 객체를 JSON 데이터로 변환하는 등의 작업을 매우 효율적으로 처리할 수 있게 해줍니다.
1️⃣ 직렬화(Serialization)
ObjectMapper 를 사용하여 자바 객체를 JSON 문자열로 직렬화하는 과정은 다음과 같습니다.
import com.fasterxml.jackson.databind.ObjectMapper;
// 예시 자바 객체
pulbic class User {
public String name;
public int age;
}
// 직렬화 예제
ObjectMapper mapper = new ObjectMapper();
User user = new User();
user.name = "Kobe";
user.age = "30";
String json = mapper.writeValueAsString(user); // 자바 객체를 JSON 문자열로 변환
System.out.println(json);
2️⃣ 역직렬화(Deserialization)
ObjectMapper 를 사용하여 JSON 문자열을 자바 객체로 역직렬화하는 과정은 다음과 같습니다.
import com.fasterxml.jackson.databind.ObjectMapper;
// 예시 자바 객체
public class User {
public String name;
public int age;
}
// 역직렬화 예제
ObjectMapper mapper = new ObjectMapper();
String json = "{\"name\":\"Kobe\", \"age\":30}";
User user = mapper.readValue(json, User.class); // JSON 문자열을 자바 객체로 변환
Systeom.out.println(user.name + " is" + user.age + " year old.");
3️⃣ 주요 기능
다양한 데이터 포맷 지원 : ObjectMapper 는 JSON 외에도 XML, CSV 등 여러 데이터 포맷을 지원합니다.(Jackson 데이터 포맷 모듈 설치 필요).
유연성과 설정 : ObjectMapper 는 맞춤 설정이 가능하여, 다양한 JSON 직렬화/역직렬화 방법을 지원합니다.
예를 들어, 필드 이름의 자동 감지, 날짜 형식 지정, 무시할 필드 설정 등을 조정할 수 있습니다.
성능 : Jackson은 JSON 처리를 위해 최적화된 라이브러리 중 하나로, 대용량 데이터 처리에도 뛰어난 성능을 보입니다.
🤔 직렬화와 역직렬화란?
직렬화(Serialization)와 역직렬화(Deserialization)는 데이터 구조 또는 객체 상태를 저장하고 전송하기 위해 다루기 쉬운 데이터 포맷으로 변환하는 과정을 의미합니다.
컴퓨터 과학의 맥락에서 이 개념은 특히 중요하며, 객체 지향 프로그래밍에서 널리 사용됩니다.
1️⃣ 직렬화(Serialization)
직렬화는 객체의 상태(즉, 객체가 가진 데이터와 그 구조)를 일련의 바이트로 변환하는 과정입니다.
이 바이트 스트림은 나중에 파일, 데이터베이스 또는 네트워크를 통해 쉽게 저장하거나 전송할 수 있습니다.
예를 들어, 자바에서는 Serialization 인터페이스를 구현한 객체를 바이트 스트림으로 변환하여 파일 시스템에 저장하거나 네트워크를 통해 다른 시스템으로 보낼 수 있습니다.
2️⃣ 직렬화의 주요 목적.
영속성 : 객체의 상태를 영구적으로 저장하여 나중에 다시 로드할 수 있습니다.
네트워크 전송 : 객체를 네트워크를 통해 다른 시스템으로 전송하기 위해 사용됩니다.
데이터 교환 : 다양한 언어나 플랫폼 간의 데이터 교환이 가능하도록 합니다.
3️⃣ 역직렬화(Deserialization)
역직렬화는 직렬화된 바이트 스트림을 다시 원래의 객체 상태로 복원하는 과정입니다.
즉, 파일, 데이터베이스 또는 네트워크로부터 바이트 스트림을 읽어 들여서 실행 중인 프로그램에서 사용할 수 있는 실제 객체로 변환합니다.
이 과정은 직렬화의 반대 과정으로, 복원된 객체는 원복 객체와 동일한 상태를 가집니다.
4️⃣ 역직렬화의 주요 사용 사례.
객체 복원 : 저장되거나 전송된 데이터로부터 객체를 재구성합니다.
상태 복구 : 애플리케이션의 이전 상태를 복구하는 데 사용됩니다.
데이터 접근 : 다른 시스템에서 전송된 데이터를 로컬 시스템에서 접근하고 사용할 수 있게 합니다.
5️⃣ 데이터 포맷과 직렬화 도구
다양한 데이터 포맷(JSON, XML, YAML 등)과 여러 프로그래밍 언어 또는 라이브러리에서 직렬화와 역직렬화를 지원합니다.
자바에서는 ObjectMapper 를 사용해 JSON 데이터 포맷으로의 직렬화와 역직렬화를 처리하며, 이는 데이터를 쉽게 읽고 쓸 수 있는 구조로 만드는 데 유용합니다.
-
-
☕️[Java] attribute의 의미와 역할
☕️[Java] attribute의 의미와 역할.
Java 백엔드 개발에서 “attribute”라는 용어는 몇 가지 다른 맥락에서 사용될 수 있습니다.
주로 두 가지 의미로 사용되는 경우가 많은데, 클래스의 속성 을 의미하는 경우와 웹 개발에서 HTTP 요청이나 세션과 관련된 데이터를 지칭하는 경우입니다.
1️⃣ 클래스의 속성(Field or Property)
Java에서 클래스의 “attribute” 는 해당 클래스의 상태를 정의하는 변수를 말합니다.
이러한 변수들은 객체의 데이터이터를 저장하고, 클래스의 인스턴스들이 갖는 특징과 상태 정보를 나타냅니다.
예를 들어, ‘Person’ 클래스가 있다면, ‘name’, ‘age’ 같은 필드들이 이 클래스의 “attribute” 가 됩니다.
public class Person {
private String name; // Attribute
private int age; // Attribute
// Constructors, getters, setters 등
}
2️⃣ 웹 개발에서의 Attribute
웹 개발에서 “attribute” 는 주로 세션(Session)이나 요청(Request) 객체에 저장된 데이터를 지칭 합니다.
이 데이터는 사용자가 웹 사이트를 이용하는 동안 지속되거나 요청 동안에만 존재할 수 있습니다.
예를 들어, 사용자가 로그인을 하면 그 사용자의 정보를 세션 attribute로 저장하여 다른 페이지에서도 사용자 정보를 유지할 수 있게 합니다.
// 세션에 사용자 정보 저장
request.getSession().setAttribute("user", userObject);
// 세션에서 사용자 정보 가져오기
User user = (User) request.getSession().getAttribute("user");
이 두 가지 사용 사례는 Java 백엔드 개발에서 매우 흔하게 접할 수 있으며, 각각의 맥락에서 attribute가 가지는 의미와 역할을 이해하는 것은 중요합니다.
첫 번째 경우는 객체 지향 프로그래밍의 핵심 요소로 클래스의 속성을 정의합니다.
두 번째 경우에는 웹 애플리케이션의 상태 관리를 돕는 수단으로서 활용됩니다.
-
-
-
-
-
-
📝[Post] 자바다식(Java多識) - 2
자바다식(Java多識) 2편.
1. @AfterEach 어노테이션.
@AfterEach 어노테이션은 JUnit 5에서 제공하는 기능입니다.
각 테스트 메서드가 실행된 후에 수행되어야 하는 작업을 지정하는 데 사용됩니다.
이 어노테이션은 테스트 클래스 내의 메서드에 적용하여 테스트 메서드가 끄탄 후 필요한 정리 작업(cleanup)을 수행할 수 있도록 합니다.
주요 역할
1. 자원 해제 : 테스트 메서드가 사용한 자원(예: 파일, 데이터베이스 연결, 네트워크 연결 등)을 해제하는 데 사용됩니다.
2. 상태 초기화 : 테스트가 완료된 후 상태를 초기화하여 다음 테스트가 깨끗한 환경에서 실행될 수 있도록 합니다.
3. 로그 기록 : 테스트 실행 결과를 로그에 기록하거나 추가적인 분석을 위해 데이터를 저장하는 데 사용할 수 있습니다.
예제 코드
아래는 @AfterEach 어노테이션을 사용한 간단한 예제입니다.
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class MyTest {
@BeforeEach
void setUp() {
System.out.println("Setting up before each test");
}
@Test
void testMethod1() {
System.out.println("Executing test method 1");
}
@Test
void testMethod2() {
Systemo.out.println("Executing test method 2");
}
@AfterEach
void tearDown() {
System.out.println("Tearing down after each test")
}
}
실행 순서
setUp() : 각 테스트 메서드 실행 전 @BeforeEach 메서드가 호출됩니다.
testMethod1() : 첫 번째 테스트 메서드가 실행됩니다.
tearDown() : 첫 번째 테스트 메서드 실행 후 @AfterEach 메서드가 호출됩니다.
setUp() : 두 번째 테스트 메서드 실행 전 @BeforeEach 메서드가 다시 호출됩니다.
testMethod2() : 두 번째 테스트 메서드가 실행됩니다.
tearDown() : 두 번째 테스트 메서드 실행 후 @AfterEach 메서드가 호출됩니다.
요약
@AfterEach 어노테이션은 각 테스트 메서드 실행 후 호출되는 메서드를 지정합니다.
주로 자원 해제, 상태 초기화, 로그 기록 등의 작업을 수행하는 데 사용됩니다.
각 테스트 메서드마다 실행되므로, 테스트 간의 독립성을 유지하고 깨끗한 테스트 환경을 보장할 수 있습니다.
2. @Builder 어노테이션.
@Builder 는 Lombok 라이브러리에서 제공하는 어노테이션으로, 빌더 패턴을 간편하게 사용할 수 있도록 지원합니다.
빌더 패턴은 객체의 생성과 관련된 복잡성을 줄이고, 가독성을 높이며, 가변 객체를 만들지 않도록 도와줍니다.
특히, 많은 필드를 가진 객체를 생성할 때 유용합니다.
주요 특징 및 역할.
유연한 객체 생성
빌더 패턴을 사용하면 객체를 생성할 때 생성자나 정적 팩토리 메서드보다 더 유연하게 객체를 구성할 수 있습니다.
필요한 필드만 설정할 수 있고, 설정 순서에 구애받지 않습니다.
가독성 향상
많은 필드를 가진 객체를 생성할 때, 빌더 패턴을 사용하면 코드의 가독성이 높아집니다.
각 필드의 이름을 명시적으로 설정할 수 있어 어떤 값이 어떤 필드에 설정되는지 쉽게 할 수 있습니다.
불변 객체 생성
빌더 패턴을 사용하면 불변 객체를 쉽게 생성할 수 있습니다.
객체가 생성된 후에는 필드 값을 변경할 수 없습니다.
사용 예시
Lombok 없이 빌더 패턴 구현
public class User {
private final String name;
private final int age;
private final String email;
private User(UserBuilder builder) {
this.name = builder.name;
this.age = builder.age;
this.email = builder.email;
}
public static class UserBuilder {
private String name;
private int age;
private String email;
public UserBuilder setName(String name) {
this.name = name;
return this;
}
public UserBuilder setAge(int age) {
this.age = age;
return this;
}
public UserBuilder setEmail(String email) {
this.email = email;
return this;
}
public User build() {
return new User(this);
}
}
}
Lombok을 사용한 빌더 패턴 구현
Lombok의 @Builder 어노테이션을 사용하면 위의 코드가 크게 단축됩니다.
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class User {
private String name;
private int age;
private String email;
}
객체 생성 예시
위의 Lombok을 사용한 User 클래스를 이용해 객체를 생성하는 예시입니다.
public class Main {
public static void main(String[] args) {
User user = User.builder()
.name("devKobe")
.age(77)
.email(devKobe@gamil.com)
.build();
System.out.println(user.getName()); // devKobe
System.out.println(user.getAge()); // 77
System.out.println(user.getEmail()) // devKobe@gmail.com
}
}
위 예시처럼 Lombok의 @Builder 를 사용하면 빌더 패턴을 간단하게 구현하고 사용할 수 있습니다.
이로 인해 객체 생성 코드가 더 깔끔하고 직관적으로 변합니다.
-
📝[Post] 자바다식(Java多識) - 1
자바다식(Java多識) 1편.
1. ‘mainClassName’ 속성 추가.
메인 클래스의 경로를 지정해주는 속성을 추가하는 방법입니다.
초기 진입점을 지정해준다고 생각하면 됩니다.
application 블록 안에 메인 클래스 이름을 지정합니다. 예를 들어, 메인 클래스가 com.example.Main 이라고 가정합니다.
아래의 코드는 bundle.gradle 파일 내부에서 수정해야 합니다.
plugins {
id 'java'
id 'application'
}
application {
mainClassName = 'com.example.Main' // 여기에 메인 클래스의 경로를 입력합니다.
applicationDefaultJvmArgs = [
"-XX:+EnableDynamicAgentLoading",
"-Djdk.instrument.traceUsage"
]
}
repositories {
mavenCentral()
}
dependencies {
// Your dependencies here
}
메인 클래스 예시
예를 들어, 메인 클래스는 다음과 같이 생겼을 수 있습니다.
package com.example;
public class Main {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
2. @ExtendWith 어노테이션.
@ExtendWith 어노테이션은 Junit 5에서 제공하는 기능으로, 테스트 클래스나 메서드에 확장 기능을 추가할 수 있도록 해줍니다.
JUnit 5의 확장 모델은 다양한 확장 기능을 통해 테스트 실행의 특정 지점에서 사용자 정의 동작을 수행할 수 있게 합니다.
@ExtendWith 어노테이션의 역할
확장 클래스 지정 : @ExtendWith 어노테이션은 확장 클래스를 지정할 수 있습니다. 지정된 확장 클래스는 테스트 라이프사이클의 특정 지점에서 호출됩니다.
예를 들어, 테스트 실행 전후, 각 테스트 메서드 전후 등 다양한 시점에서 특정 동작을 추가할 수 있습니다.
컨텍스트 설정 및 주입 : 확장 기능을 통해 테스트 컨텍스트를 설정하고, 테스트 메서드에 필요한 객체나 리소스를 주입할 수 있습니다. 이를 통해 테스트 코드를 더 간결하고 모듈화할 수 있습니다.
조건부 실행 : 특정 조건에 따라 테스트 메서드를 실행하거나 건너뛸 수 있도록 지원합니다.
예를 들어, 특정 환경 설정이나 시스템 상태에 따라 테스트 실행 여부를 결정할 수 있습니다.
커스텀 어서션 및 보고 : 확장을 통해 사용자 정의 어서션 로직을 추가하거나 테스트 결과를 커스텀 방식으로 보고할 수 있습니다.
예제 코드
아래는 @ExtendWith 어노테이션을 사용한 간단한 예제입니다.
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.Test;
class MyExtension implements BeforeEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
System.out.println("Before each test method");
}
}
@ExtendWith(MyExtension.class)
public class MyTest {
@Test
void testMethod1() {
System.out.println("Test method 1");
}
@Test
void testMethod2() {
System.out.println("Test method 2");
}
}
위 코드에서 MyExtension 클래스는 BeforeEachCallback 인터페이스를 구현하여 각 테스트 메서드가 실행되기 전에 메시지를 출력합니다.
@ExtendWith(MyExtension.class) 어노테이션을 통해 MyTest 클래스에 이 확장 기능을 추가했습니다.
따라서 각 테스트 메서드 실행 전에 “Before each test method” 메시지가 출력됩니다.
이처럼 @ExtendWith 어노테이션은 JUnit 5의 확장 모델을 활용하여 테스트에 필요한 다양한 기능을 추가할 수 있게 해줍니다.
3. 어서션(Assertion)
어서션(Assertion)은 프로그래밍 및 소프트웨어 테스트에서 코드의 특정 상태나 조건이 참인지 확인하는 데 사용되는 문장이나 명령문을 의미합니다.
어서션을 통해 코드의 논리적 일관성과 정확성을 검증할 수 있으며, 주로 디버깅과 테스트에 사용됩니다.
주요 기능과 목적.
조건 검증 : 어서션(Assertion)은 특정 조건이 참인지 검증합니다. 조건이 거짓이면 프로그램은 즉시 실행을 중단하고 오류를 보고합니다.
디버깅 도구 : 어서션(Assertion)은 개발 중에 코드의 오류를 조기에 발견하고 수정하는 데 도움이 됩니다. 코드의 가정이 잘못된 경우 어서션을 통해 문제를 빨리 찾을 수 있습니다.
문서화 : 어서션(Assertion)은 코드의 논리적 전제 조건을 명시적으로 표현하여, 코드가 어떤 상태에 작동해야 하는지 명확하게 나타냅니다.
어서션(Assertion)의 예
자바(Java)
public void setAge(int age) {
assert age > 0 : "Age must be positive";
this.age = age;
}
JUnit (Java)
```java
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
public class MyTest {
@Test
void testAddition() {
int result = 2 + 3;
assertEquals(5, result, "2 + 3 should equal 5");
} } ```
어서션 사용 시기
개발 중 : 개발자가 코드의 논리적 일관성을 검증하기 위해 사용합니다. 디버깅 과정에서 주로 사용되며, 프로덕션 환경에서는 보통 비활성화합니다.
테스트 코드 : 테스트 프레임워크(JUnit, TestNG 등)를 사용하여 테스트를 작성할 때, 특정 조건이 기대한 대로 동작하는지 확인합니다.
주의 사항
프로덕션 코드에서의 사용 : 어서션은 주로 개발 및 테스트 환경에서 사용되며, 프로덕션 환경에서는 비활성화되는 경우가 많습니다. 프로덕션 환경에서 조건 검증이 필요한 경우에는 예외 처리를 사용합니다.
부작용 없는 코드 : 어서션 내부에서는 부작용이 없는 코드를 사용하는 것이 좋습니다. 어서션은 상태를 변경하지 않고 조건만 검증해야 합니다.
요약.
어서션은 코드의 특정 조건이 참임을 검증하는 도구로, 디버깅과 테스트 과정에서 코드의 논리적 일관성을 유지하는데 중요한 역할을 합니다.
이를 통해 개발자는 코드의 가정과 실제 동작이 일치하는지 확인하고, 문제를 조기에 발견하여 수정할 수 있습니다.
-
☕️[Java] 프로그래밍 언어와 자바
변수 선언.
컴퓨터 메모리(RAM)은 수많은 번지들로 구성된 데이터 저장 공간입니다.
프로그램은 데이터를 메모리에 저장하고 읽는 작업을 비번히 수행합니다.
이때 데이터를 어디에, 어떤 방식으로 저장할지 정해져 있지 않다면 메모리 관리가 무척 어려워집니다.
이 문제를 해결하기 위해 변수(Variable)을 사용합니다.
변수(Variable)는 하나의 값을 저장할 수 있는 메모리 번지에 붙여진 이름입니다.
변수를 통해 프로그램은 메모리 번지에 값을 저장하고 읽을 수 있습니다.
자바의 변수는 다양한 타입의 값을 저장할 수 없습니다.
즉, 정수형 변수에는 정수값만 저장할 수 있고, 실수형 변수에는 실수값만 저장할 수 있습니다.
변수를 사용하려면 변수 선언이 필요합니다.
변수 선언은 어떤 타입의 데이터를 저장할 것인지 그리고 변수 이름이 무었인지 결정하는 것입니다.
int age; // 정수(int) 값을 저장할 수 있는 age 변수 선언
double value; // 실수(double) 값을 저장할 수 있는 value 변수 선언
변수 이름의 첫 번째 글자가 문자여야 하고, 중간부터는 문자, 숫자, $, _를 포함할 수 있습니다.
또한, 첫 문자를 소문자로 시작하되 캐멀 케이스로 작성하는 것이 관례입니다.
변수가 선언 되었다면 값을 저장할 수 있습니다.
이때 대입 연산자인 =를 사용합니다.
수학에서 등호(=)는 ‘같다’라는 의미이지만, 자바에서는 우측 값을 좌측 변수에 대입하는 연산자로 사용됩니다.
int score; // 변수 선언
score = 60; // 값 대입
변수 선언은 저장되는 값의 타입과 이름만 결정한 것이지, 아직 메모리에 할당된 것은 아닙니다.
변수에 최초로 값이 대입될 때 메모리에 할당되고, 해당 메모리에 값이 저장됩니다.
변수에 최초로 값을 대입하는 행위를 변수 초기화라고 하고, 이때의 값을 초기값이라고 합니다.
초기 값은 다음과 같이 변수를 선언함과 동시에 대입할 수도 있습니다.
int score = 90;
초기화되지 않은 변수는 아직 메모리에 할당되지 않았기 때문에 변수를 통해 메모리 값을 읽을 수 없습니다.
따라서 다음은 잘못된 코딩입니다.
int value; // <- 1.변수 value 선언
int result = value + 10; // <- 2.변수 value 값을 읽고 10을 더해서 변수 result에 저장
1 에서 변수 value가 선언되었지만, 초기화되지 않았기 때문엔 2 value + 10에서 value 변수값은 읽어올 수 없습니다.
따라서 위 코드는 다음과 같이 변경해야 합니다.
int value = 30; // 변수 value가 30으로 초기화됨
int result = value + 10; // 변수 value 값(30)을 읽고 10을 더해서 변수 result에 저장
다음 예제는 초기화되지 않은 변수를 연산식에 사용할 경우 컴파일 에러(The local variable value may not have been initializer)가 발생하는 것을 보여줍니다.
public class VariableInitializationExample {
public static void main(String[] args) {
// 변수 value 선언
int value;
// 연산 결과를 변수 result의 초기값으로 대입
int result = value + 10; // <------- 컴파일 오류
// 변수 result 값을 읽고 콘솔에 출력
System.out.println(result);
}
}
변수는 출력문이나 연산식에 사용되어 변수값을 활용합니다.
다음 예제는 변수를 문자열과 결합 후 출력하거나 연산식에서 활용하는 모습을 보여줍니다.
```java
public class VariableUseExample {
public static void main(String[] args) {
int hour = 3;
int minute = 5;
System.out.println(hour + “시간” + minute + “분”);
int totalMinute = (hour*60) + minute;
System.out.println("총" + totalMinute + "분"); } }
// 실행 결과
// 3시간 5분
// 총 185분
- 변수는 또 다른 변수에 대입되어 메모리 간에 값을 복사할 수 있습니다.
- 다음 코드는 변수 x 값을 변수 y 값으로 복사합니다.
```java
int x = 10; // 변수 x에 10을 대입
int y = x; // 변수 y에 변수 x값을 대입
다음 예제는 두 변수의 값을 교환하는 방법을 보여줍니다.
두 변수의 값을 교환하기 위해서 새로운 변수 temp를 선언한 것에 주목합시다.
```java
public class VariableExchangeExample {
public static void main(String[] args) {
int x = 3;
int y = 5;
System.out.println(“x:” + x + “, y:” + y);
int temp = x;
x = y;
y = temp;
System.out.println(“x:” + x + “, y:” + y);
}
}
// 실행 결과
// x:3, y:5
// x:5, y:3
```
-
💾[Database] 데이터베이스의 정의와 특징.
💾[Database] 데이터베이스의 정의와 특징.
1️⃣ 데이터베이스 : 여러 사용자나 응용 프로그램이 공유하고 동시에 접근 가능한 ‘데이터의 집합’ 이라고 정의할 수 있습니다.
2️⃣ DBMS(DataBase Management System) : ‘데이터베이스’를 ‘관리,운영하는 소프트웨어’ 입니다.
🙋♂️ 데이터베이스
‘데이터 저장 공간’ 자체를 의미하기도 합니다.
DBMS 중 하나인 MySQL에서는 ‘데이터베이스’를 ‘자료가 저장되는 디스크 공간(주로 파일로 구성됨)’으로 취급합니다.
위 그림은 데이터베이스, DBMS, 사용자, 응용 프로그램의 관계를 보여줍니다.
위 그림에서 보듯이 DBMS는 데이터베이스를 관리하는 역할을 하는 소프트웨어입니다.
여러 사용자나 응용 프로그램은 DBMS가 관리하는 데이터에 동시에 접속하여 데이터를 공유합니다.
👉 즉, DBMS에서는 데이터베이스에서 사용되는 데이터가 집중 관리됩니다.
🙋♂️ 데이터베이스와 DBMS
데이터베이스를 DBMS와 혼용해서 같은 용어처럼 사용하는 경우도 흔히 있습니다.
바라보는 시각에 따라 그렇게 사용하는 것이 틀린 것은 아니지만
저는 데이터베이스를 ‘데이터의 집합’ 또는 ‘데이터의 저장 공간’으로 보고,
DBMS는 데이터베이스를 운영하는 ‘소프트웨어’라는 의미로 공부하겠습니다.
DBMS에는 MySQL 외에도 많은 종류의 프로그램이 있습니다.
MySQL
MariaDB
PostgreSQL
Oracle
SQL Server
DB2
Access
SQLite
…
🙋♂️ 위 명시된 리스트는 2018년 기준 많이 사용되는 DBMS입니다.
3️⃣ DBMS 또는 데이터베이스의 몇 가지 중요한 특징.
👉 데이터 무결성
데이터베이스 안의 데이터는 어떤 경로를 통해 들어왔든 오류가 있어서는 안 되는데 이를 무결성(Integrity)이라고 합니다.
무결성을 지키기 위해 데이터베이스는 제약 조건(constraint)을 따릅니다.
예를 들어 학생 데이터에서 모든 학생은 학번이 반드시 있어야 하고 학번이 중복되면 안 된다는 제약 조건을 생각해봅시다.
이 제약 조건을 충실히 지킨다면 학번으로도 학번으로도 학생 데이터에서 학생을 정확히 찾을 수 있습니다.
즉, 학번은 무결한 데이터를 보장하는 요소이며, 자동 발급기로 성적 증명서나 재학 증명서를 뗄 떼 학번만 조회해도 정확한 자료를 줄력할 수 있습니다.
👉 데이터의 독립성
데이터베이스의 크기를 변경하거나 데이터 파일의 저장소를 변경하더라도 기존에 작성된 응용 프로그램은 전혀 영향을 받지 않습니다.
즉 데이터베이스와 응용 프로그램은 서로 의존적인 관계가 아니라 독립적인 관계입니다.
예를 들어 데이터베이스가 저장된 디스크가 새것으로 변경되어도 기존에 사용하던 응용 프로그램은 아무런 변경 없이 계속 사용할 수 있습니다.
👉 보안
데이터베이스 안에 데이터는 아무나 접근할 수 있는 것이 아니라 데이터를 소유한 사람이나 데이터에 접근이 허가된 사람만 접근할 수 있습니다.
또한, 같은 데이터에 접근할 때도 사용자의 계정에 따라서 각각 다른 권한을 갖습니다.
최근 들어 고객 정보 유출 사고가 빈번하여 보안(Security)은 데이터베이스에서 더욱 중요한 이슈가 되고 있습니다.
👉 데이터 중복 최소화
데이터베이스에서는 동일한 데이터가 여러 군데 중복 저장되는 것을 방지합니다.
학교를 예로 들면, 학생 정보를 이용하는 교직원들(학생처, 교무처, 과사무실 등)이 각 직원마다 별도의 엑셀 파일로 학생 정보를 관리하면 한 명의 학생 정보가 각각의 엑셀 파일에 중복 저장됩니다.
그러나 데이터베이스에 통합하여 관리하면 하나의 테이블에 데이터를 저장한 후 응용 프로그램마다 이를 공유하여 사용할 수 있어 데이터의 중복을 최소화할 수 있습니다.
👉 응용 프로그램 제작 및 수정 용이
기존 파일 시스템에서는 각각의 파일 포맷에 맞춰 응용 프로그램을 개발했습니다.
그러나 데이터베이스를 이용하면 통일된 방식으로 응용 프로그램을 작성할 수 있고 유지,보수 또한 쉽습니다.
👉 데이터의 안전성 향상
대부분의 DBMS는 데이터의 백업/복원 기능을 제공합니다.
따라서 데이터가 손상되는 문제가 발생하더라도 원래의 상태로 복원 또는 복구할 수 있습니다.
-
-
-
-
-
📝[Post] 서버와 클라이언트의 개념(1)
🙋♂️ Preview
이번 포스트에서는 컴퓨터 과학에서 말하는 서버와 클라이언트의 개념을 크게 세 가지로 나눠 살펴보겠습니다.
이것은 이해를 돕기 위한 분류로, 서버와 클라이언트라는 개념에 익숙해지고 난 후에 다시 보면 왜 이렇게 나누었는지 이해가 될 것 입니다.
1️⃣ 네트워크에서의 서버와 클라이언트.
서버(Server) : “서비스를 제공하는 쪽”
클라이언트(Client) : “서비스를 제공받는 쪽”
그림에서 서버는 실제 존재하는 물리적인 고성능 컴퓨터이고, 클라이언트는 데스크톱이나 노트북, 스마트폰 등과 같은 사용자들의 단말기를 나타냅니다.
즉, 물리적 장치와 또 다른 물리적 장치 사이의 관계를 의미합니다.
이렇게 물리적인 장치 간에 서로 통신이 이루어지기 위해서는 “통신을 시작하는 쪽”이 “상대방의 네트워크 주소인 IP 주소를 알고 있어야 합니다.”
“클라이언트가 서버의 IP주소를 알고있어야 서버와 클라이언트로서의 관계를 맺을 수 있습니다.”
1️⃣ 트래픽(Traffic) 처리 방법.
우리가 컴퓨터나 스마트폰으로 이용하는 서비스들은 수백만 명 이상의 사용자가 동시에 사용하고 있는 경우가 대부분입니다.
그렇다면 이러한 서비스를 운영하는 서버가 모두 고성능일까요? 🤔
당연히 그렇지 않습니다 ❌
한꺼번에 수백만 명 이상의 사용자로부터 생기는 “트래픽(Traffic)”을 처리하기 위한 방법은 여러가지가 있습니다.
여기서는 가장 범용적이고 직관적인 방법 두 가지, “로드 밸런싱” 과 “캐시”에 대해 간단히 설명하겠습니다.
1️⃣ 로드 밸런싱(Load Balancing).
“로드 밸런싱(Load Balancing)” : 부하 분산.
즉, 서버에 가해지는 부하(Load)를 분산하는 것입니다.
사용자들의 트래픽을 여러 서버가 나눠 받도록 구성하며, 일반적으로 네트워크 장비인 “스위치(Switch)” 를 할당해 “로드 밸런싱”할 수 있습니다.
스위치에서 어떤 서버로 로드 밸런싱이 되도록 할지는 소프트웨어적으로 제어할 수 있습니다.
“로드 밸런싱” 은 “스위치” 라는 장비가 “클라이언트의 트래픽을 먼저 받아” 서 여러 대의 서버로 “분산” 해 주는 방식입니다.
이렇게 하면 부하가 분산되는 효과 외에도 스위치 뒤에 연결된 서버들을 필요에 따라 추가하거나 삭제할 수 있어 편리합니다.
2️⃣ 캐시(Cache).
“캐시(Cache)” : 비용이 큰 작업의 결과를 어딘가에 저장하여 비용이 작은 작업으로 동일한 효과를 내는 것.
캐시를 이용하면 매번 요청이 들어올 때마다 비용이 큰 작업을 다시 수행할 필요 없이 미리 저장된 결과로 응답하면 됩니다.
물론 이렇게 하면 가장 최신의 데이터는 아닐 수 있지만, 성능을 극대화시키고자 하는 캐시의 목적을 생각해 데이터의 실시간성을 조금 포기해도 되는 경우가 많습니다.
✏️ Example
음원 서비스
데이터베이스에 저장된 수많은 음원의 다운로드 수, 스트리밍 수, 추천 수 등으로 인기 점수를 계산하려 100갸의 곡을 오름차순 순위로 제공합니다.
만약 사용자가 한 번 음원을 조회할 때마다 모든 음원의 인기 점수를 계산해 순위를 매긴다면 아마 사용자가 수백 명만 되어도 서버 부하로 응답 시간이 매우 느려질 것입니다.
이렇게 수많은 음원의 인기 점수를 매번 계산하여 순위를 매기는 작업이 바로 ‘비용이 큰 작업’ 입니다.
매시 정각마다 TOP 100을 계산한 결과를 저장했다가 사용자의 요청이 들어왔을 때 응답해주면 ‘비용이 작은 작업’으로 대체할 수 있습니다.
사용자는 16시 30분에 16시에 저장된 TOP 100 결과로도 큰 불편함을 느끼지 않습니다.
이렇게 사용자가 캐시된 과거의 데이터를 보더라도 서비스 시용에 지장이 없다면 캐시 사용을 충분히 고려할 만합니다.
“캐시” 는 다양한 상황에서 비슷한 뜻으로 사용되지만, 공통적으로, ‘비용이 큰 작업을 비용이 작은 작업으로 대신하는 것’이라고 정리할 수 있습니다.
-
-
📚[ENG][240621] 제목만 해석하는 영어 공부 :)
1️⃣ Why You Should Stop Using @Value Annotations In Spring (And Use This Instead)
🙋♂️ 해석: “Spring에서 @Value 어노테이션(주석)을 사용을 중단하고 (대신 이를 사용해야 하는 이유)”
📝Reference
2️⃣ Be part of a better internet
🙋♂️ 해석: “더 나은 인터넷의 일원이 되세요.”
Be part of : “일원이 되다” 또는 “참여하다” 라는 뜻을 가지고 있습니다. 어떤 단체나 활동, 또는 상황에 참여하거나 속하는 것을 의미합니다.
예를 들어 “Be part of team”은 “팀의 일원이 되다”라는 뜻이 됩니다.
📝Reference
3️⃣ Unpacking the “Day Job”
🙋♂️ 해석: "’본업’을 해부하기”
“Unpacking” : 문자 그대로는 “짐을 풀다”라는 뜻이지만, 비유적으로는 어떤 주제나 개념을 자세히 분석하거나 설명하는 것을 의미합니다.
예를 들어, “Unpacking the ‘Day Job’“은 “본업에 대해 자세히 분석하기” 또는 “본업을 해부하기”라는 의미로 이해할 수 있습니다.
📝Reference
4️⃣ 10 Cheap Desk Upgrades Every Programmer Needs #DeskSeries
🙋♂️ 해석: “모든 프로그래머가 필요한 저렴한 책상 업그레이드 10가지”
📝Reference
5️⃣ These Dividend Sell-Offs Could Mean Higher Starting Yields For You!
🙋♂️ 해석: “이 배당금 매도는 더 높은 초기 수익률을 의미할 수 있습니다!”
“Dividend” : 주식시장에서 “배당금” 을 의미합니다.
이는 기업이 이익의 일부를 주주들에게 분배하는 금액입니다. 배당금은 주로 보통 현금으로 지급되지만, 주식 형태로 지급되기도 합니다. 배당금은 주로 정기적으로, 예를 들어 분기별이나 연간으로 지급됩니다.
“Sell-Offs” : 금융 시장에서 “대규모 매도” 를 의미합니다.
이는 투자자들이 대량으로 자산을 매도하여 시장에 공급이 급증하고, 그로 인해 가격이 하락하는 상황을 말합니다. 주식, 채권, 상품 등 다양한 자산에서 발생할 수 있습니다.
“Yields” : 금융 및 투자 분야에서 “수익률” 을 의미합니다.
이는 투자로부터 얻을 수 있는 수익의 비율을 나타내며, 보통 퍼센트로 표시됩니다. 수익률은 다양한 방식으로 계산될 수 있으며, 주식의 경우 배당금 수익률, 채권의 경우 이자 수익률 등이 이에 해당합니다. 일반적으로 수익률은 투자자에게 해당 자산이 얼마나 수익을 창출할 수 있는지를 보여주는 중요한 지표입니다.
📝Reference
-
-
-
-
-
-
-
-
-
-
-
-
📦[DS,Algorithm] Circular Queue(원형 큐)의 중간 지점 찾기.
1️⃣ Circular Queue(원형 큐)의 중간 지점 찾기.
Java에서 배열을 사용하여 구현한 원형 큐에서 중간 지점을 찾는 방법은 큐의 시작 위치(‘front’)와 끝 위치(‘rear’)를 기준으로 계산할 수 있습니다.
중간 지점을 찾는 공식은 원형 큐의 특성을 고려하여 적절히 조정되어야 합니다.
2️⃣ 중간 지점을 찾기 위한 방법.
1️⃣ 중간 지점 계산 공식.
중간 지점을 찾는 방법은 큐의 시작점과 끝점을 이용하여 계산할 수 있습니다.
원형 큐의 크기, 시작 인덱스(front), 끝 인덱스(rear)를 사용하여 중간 인덱스를 계산할 수 있습니다.
이때 중간 지점을 계산하는 공식은 다음과 같습니다.
(front + size / 2) % capacity
여기서 ‘size’ 는 큐에 현재 저장된 요소의 수이고, ‘capacity’ 는 큐의 전체 크기입나다.
3️⃣ 예시
public class CircularQueue {
private int[] queue;
private int front, rear, size, capacity;
public CircularQueue(int capacity) {
this.capacity = capacity;
this.queue = new int[capacity];
this.front = 0;
this.rear = 0;
this.size = 0;
}
public boolean isFull() {
return size == capacity;
}
public boolean isEmpty() {
return size == 0;
}
public void enqueue(int data) {
if (isFull()) {
throw new RuntimeException("Queue is full");
}
queue[rear] = data;
rear = (rear + 1) % capacity;
size++;
}
public int dequeue() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
int data = queue[front];
front = (front + 1) % capacity;
size--;
return data;
}
public int getMiddle() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
int middleIndex = (front + size / 2) % capacity;
return queue[middleIndex];
}
public static void main(String[] args) {
CircularQueue cq = new CircularQueue(5);
cq.enqueue(10);
cq.enqueue(20);
cq.enqueue(30);
cq.enqueue(40);
cq.enqueue(50);
System.out.println("Middle element: " + cq.getMiddle()); // Output: Middle element: 30
cq.dequeue();
cq.enqueue(60);
System.out.println("Middle element: " + cq.getMiddle()); // Output: Middle element: 40
}
}
이 코드에서는 ‘CircularQueue’ 클래스를 정의하고, ‘enqueue’, ‘dequeue’, ‘isFull’, ‘isEmpty’ 메서드를 포함합니다.
또한, 큐의 중간 요소를 반환하는 ‘getMiddle’ 메서드를 정의합니다.
이 메서드는 현재 큐의 크기와 시작 인덱스를 사용하여 중간 인덱스를 계산한 후 해당 인덱스의 요소를 반환합니다.
-
-
-
📦[DS,Algorithm] Deque에서의 front와 rear의 변화.
🧨 시발점.
Deque을 공부하던 중 동적으로 변하는 front와 rear가 근본적으로 어떻게 동작하는지 궁금해졌습니다.
이것을 알게되면 정확하게 Deque의 addFirst, addLast, removeFirst, removeLast 시 front와 rear가 어디에 위치하는지 알 수 있고 Deque의 원리를 이해 할 수 있을 것 같았습니다.
1️⃣ Deque의 front와 rear의 위치는 변할 수 있나요? 🤔
‘Deque‘ (Double Ended Queue)에서 ‘front‘ 와 ‘rear‘ 의 위치는 변할 수 있습니다.
‘Deque‘ 는 양쪽 끝에서 삽입과 삭제가 모두 가능한 자료구조이기 때문에, ‘front‘ 와 ‘rear‘ 의 위치는 데이터가 삽입되거나 제거될 때마다 변합니다.
2️⃣ Deque에서의 front와 rear의 변화. 🤩
1️⃣ 삽입 연산 (‘addFirst‘ 와 ‘addLast‘)
‘addFirst’ : 요소를 덱의 앞쪽에 삽입합니다.
‘front‘ 위치가 바뀝니다.
‘addLast’ : 요소를 덱의 뒤쪽에 삽입합니다.
‘rear‘ 위치가 바뀝니다.
2️⃣ 삭제 연산 (‘removeFirst‘ 와 ‘removeLast‘)
‘removeFirst’ : 덱의 앞쪽에서 요소를 제거합니다.
‘front‘ 위치가 바뀝니다.
‘removeLast’ : 덱의 뒤쪽에서 요소를 제거합니다.
‘rear‘ 위치가 바뀝니다.
3️⃣ 예제 코드.
아래는 ‘Deque’ 의 ‘LinkedList’ 구현을 사용하여 ‘front’ 와 ‘rear’ 의 변화를 보여주는 예제 코드입니다.
import java.util.Deque;
import java.util.LinkedList;
public class DequeExample {
public static void main(String[] args) {
Deque<String> deque = new LinkedList<>();
// 요소를 덱의 앞과 뒤에 추가
deque.addFirst("A"); // front: A
deque.addLast("B"); // rear: B
deque.addFirst("C"); // front: C, rear: B
deque.addLast("D"); // rear: D
System.out.println("Initial Deque: " + deque); // 출력 : [C,A,B,D]
// 앞쪽에서 요소 제거
System.out.println("Removed from front: " + deque.removeFirst()); // 출력: C
// 뒤쪽에서 요소 제거
System.out.println("Removed from rear: " + deque.removeLast()); // 출력: D
System.out.println("Deque after removals: " + deque); // 출력: [A, B]
// 덱의 앞쪽과 뒤쪽에서 요소 확인
System.out.println("Front element: " + deque.getFirst()); // 출력: A
System.out.println("Rear element: " + deque.getLast()); // 출력: B
}
}
👉 설명.
1️⃣ 삽입 연산.
‘deque.addFirst(“A”)’ : “A”를 덱의 앞에 삽입합니다.
‘deque.addLast(“B”)’ : “B”를 덱의 뒤에 삽입합니다.
‘deque.addFirst(“C”)’ : “C”를 덱의 앞에 삽입합니다.
‘deque.addLast(“D”)’ : “D”를 덱의 뒤에 삽입합니다.
이 연산들은 ‘front’ 와 ‘rear’ 의 위치를 업데이트합니다.
2️⃣ 삭제 연산.
‘deque.removeFirst()’ : 덱의 앞쪽에서 “C”를 제거합니다.
‘deque.removeLast()’ : 덱의 뒤쪽에서 “D”를 제거합니다.
이 연산들은 ‘front’ 와 ‘rear’ 의 위치를 다시 업데이트합니다.
3️⃣ 요소 확인.
‘deque.getFirst()’ : 덱의 앞쪽 요소를 확인합니다.
‘deque.getLast()’ : 덱의 뒤쪽 요소를 확인합니다.
이 예시 코드는 ‘front’ 와 ‘rear’ 가 데이터의 삽입 및 삭제 연산에 따라 어떻게 변하는지 잘 보여줍니다.
‘Deque’ 는 이처럼 양쪽 끝에서의 삽입과 삭제 연산을 지원하므로, ‘front’ 와 ‘rear’ 의 위치는 동적입니다.
-
-
☕️[Java] 다형성(Polymorphism)
1️⃣ 다형성(Polymorphism).
‘다형성(Polymorphism)’ 은 ‘객체 지향 프로그래밍(OOP)’ 의 중요한 개념 중 하나로, 같은 인터페이스를 통해 서로 다른 데이터 타입의 객체를 조작할 수 있도록 합니다.
다형성은 코드의 재사용성과 유연성을 높여주며, 유지보수를 쉽게 해줍니다.
Java에서 ‘다형성’ 은 주로 ‘상속’ 과 ‘인터페이스’ 를 통해 구현됩니다.
2️⃣ 다형성의 개념.
다형성은 “하나의 인터페이스로 여러 가지 형태를 구현할 수 있는 능력” 을 의미합니다.
이는 같은 메서드가 다양한 객체에서 다르게 동작할 수 있게 합니다.
3️⃣ 다형성의 두 가지 형태.
1️⃣ 컴파일 시간 다형성(Compile-time Polymorphism)
메서드 오버로딩(Method Overloading)을 통해 구현됩니다.
컴파일 시점에 어떤 메서드가 호출될지 결정됩니다.
같은 이름의 메서드를 여러 개 정의하지만, 매개변수의 타입이나 개수가 달라야 합니다.
2️⃣ 런타임 다형성 (Runtime Polymorphism)
메서드 오버라이딩(Method Overriding)을 통해 구현됩니다.
실행 시점에 어떤 메서드가 호출될지 결정됩니다.
부모 클래스의 메서드를 자식 클래스에서 재정의하여 사용합니다.
4️⃣ 컴파일 시간 다형성(Method Overloading).
메서드 오버로딩은 같은 클래스 내에서 같은 이름을 가진 메서드를 여러 개 정의하는 것입니다.
단, 매개변수의 수나 타입이 달라야 합니다.
💻 예제.
public class MathOperations {
// 정수 두 개의 합
public int add(int a, int b) {
return a + b;
}
// 실수 두 개의 합
public double add(double a, double b) {
return a + b;
}
// 새 개의 정수의 합
public int add(int a, int b, int c) {
return a + b + c;
}
public static void main(String[] args) {
MathOperations mathOperations = new MathOperations();
System.out.println(mathOperations.add(1, 2)); // 3
System.out.println(mathOperations.add(1.5, 2.5)); // 4.0
System.out.println(mathOperations.add(1, 2, 3)); // 6
}
}
5️⃣ 런타임 다형성(Method Overriding).
메서드 오버라이딩은 자식 클래스가 부모 클래스의 메서드를 재정의하는 것을 말합니다.
이를 통해 자식 클래스의 객체가 부모 클래스의 메서드를 호출할 때, 자식 클래스의 메서드가 실행되도록 합니다.
💻 예제.
class Animal {
void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Cat meows");
}
}
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog(); // Animal 타입으로 Dog 객체 생성
Animal myCat = new Cat(); // Animal 타입으로 Cat 객체 생성
myDog.makeSound(); // Dog barks
myCat.makeSound(); // Cat meows
}
}
6️⃣ 인터페이스를 통한 다형성.
인터페이스를 통해서도 다형성을 구현할 수 있습니다.
인터페이스는 메서드의 서명만을 정의하며, 이를 구현하는 클래스가 메서드의 구체적인 동작을 정의합니다.
💻 예제.
interface Shape {
void draw();
}
class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Circle");
}
}
class Square implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Square");
}
}
public class Main {
public static void main(String[] args) {
Shape myShape1 = new Circle();
Shape myShape2 = new Square();
myShape1.draw(); // Drawing a Circle
myShape2.draw(); // Drawing a Square
}
}
7️⃣ 다형성의 장점.
코드 재사용성 : 상위 클래스나 인터페이스를 사용하여 다양한 하위 클래스나 구현체를 다룰 수 있어 코드의 재사용성이 높아집니다.
유연성 : 새로운 클래스나 기능을 추가할 때 기존 코드를 수정할 필요 없이 확장할 수 있습니다.
유지보수성 : 코드를 이해하고 유지보수하는 것이 더 쉬워집니다. 메서드의 호출이 어디서 어떻게 이루어지는지 명확하기 때문입니다.
8️⃣ 예제: 다형성의 실질적 사용.
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class PolymorphismExample {
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>();
List<String> linkedList = new LinkedList<>();
arrayList.add("ArrayList Item");
linkedList.add("LinkedList Item");
printList(arrayList); // ArrayList Item
printList(linkedList); // LinkedList Item
}
public static void printList(List<String> list) {
for (String item : list) {
System.out.println(item);
}
}
}
이 예제에서는 ‘List‘ 인터페이스를 사용하여 ‘ArrayList‘ 와 ‘LinkedList‘ 를 동일한 방식으로 처리합니다.
이를 통해 다양한 구현체를 다룰 수 있는 유연한 코드를 작성할 수 있습니다.
📝 결론.
다형성은 객체 지향 프로그래밍의 핵심 개념 중 하나로, 코드의 유연성과 재사용성을 크게 향상시킵니다.
이를 통해 다양한 형태의 객체를 동일한 방식으로 다룰 수 있으며, 새로운 기능을 쉽게 확장하고 유지보수할 수 있습니다.
다형성은 상속과 인터페이스를 통해 구현되며, 메서드 오버로딩과 오버라이딩을 통해 다양한 형태를 취할 수 있습니다.
-
📦[DS,Algorithm] Circular Queue(원형 큐)란?
1️⃣ Circular Queue(원형 큐)란?
원형 큐는 큐의 일종으로, 배열을 사용하여 구현되며, 큐의 마지막 위치가 처음 위치와 연결되어 원형 구조를 가지는 큐입니다.
원형 큐는 고정된 크기의 배열을 사용하여 구현되므로, 큐의 마지막 인덱스가 배열의 끝에 도달하면 다음 인덱스가 배열의 시작 부분으로 이동합니다.
이를 통해 메모리를 효율적으로 사용할 수 있으며, 큐의 처음과 끝을 관리하는 데 도움이 됩니다.
2️⃣ 원형 큐의 원리.
고정된 크기 : 원형 큐는 고정된 크기의 배열을 사용하여 구현됩니다. 따라서 배열의 크기를 초과하여 요소를 추가할 수 없습니다.
연결된 인덱스 : 큐의 마지막 인덱스가 배열의 끝에 도달하면, 다음 인덱스는 배열의 처음 부분으로 이동합니다.
두 개의 포인터 : 원형 큐는 두 개의 포인터를 사용하여 구현됩니다.
‘front’ : 큐의 첫 번째 요소를 가리킵니다.
‘rear’ : 큐의 마지막 요소를 가리킵니다.
비어 있는 상태와 가득 찬 상태 : 큐가 비어 있는 상태와 가득 찬 상태를 구별해야 합니다. 이를 위해 추가적인 변수를 사용하거나 포인터의 위치를 비교하여 상태를 확인합니다.
3️⃣ 원형 큐의 주요 연산.
초기화 : 큐의 크기를 설정하고, ‘front’ 와 ‘rear’ 포인터를 초기화합니다.
isEmpty() : 큐가 비어 있는지 확인합니다.
isFull() : 큐가 가득 찼는지 확인합니다.
enqueue() : 큐에 요소를 추가합니다. ‘rear’ 포인터를 업데이트합니다.
dequeue() : 큐에서 요소를 제거하고 반환합니다. ‘front’ 포인터를 업데이트합니다.
peek() : 큐의 첫 번째 요소를 반환합니다.
4️⃣ 원형 큐의 예제 구현.
public class CircularQueue {
private int[] queue;
private int front;
private int rear;
private int size;
private int capacity;
// 생성자
public CircularQueue(int capacity) {
this.capacity = capacity;
queue = new int[capacity];
front = 0;
rear = -1;
size = 0;
}
// 큐가 비어 있는지 확인
public boolean isEmpty() {
return size == 0;
}
// 큐가 가득 찼는지 확인
public boolean isFull() {
return size == capacity;
}
// 큐에 요소 추가
public void enqueue(int element) {
if (isFull()) {
System.out.println("Queue is full");
return;
}
rear = (rear + 1) % capacity;
queue[rear] = element;
size++;
}
// 큐에서 요소 제거
public int dequeue() {
if (isEmpty()) {
System.out.println("Queue is empty");
return -1;
}
int element = queue[front];
front = (front + 1) % capacity;
size--;
return element;
}
// 큐의 첫 번째 요소 확인
public int peek() {
if (isEmpty()) {
System.out.println("Queue is empty");
return -1;
}
return queue[front];
}
// 큐의 크기 반환
public int getSize() {
return size;
}
// 큐의 모든 요소 출력
public void display() {
if (isEmpty()) {
System.out.println("Queue is empty");
return;
}
int i = front;
int count = 0;
while (count < size) {
System.out.print(queue[i] + " ");
i = (i + 1) % capacity;
count++;
}
System.out.println();
}
// 메인 메서드 (테스트용)
public static void main(String[] args) {
CircularQueue cq = new CircularQueue(5);
cq.enqueue(10);
cq.enqueue(20);
cq.enqueue(30);
cq.enqueue(40);
cq.enqueue(50);
cq.display(); // 출력: 10 20 30 40 50
System.out.println("Dequeued: " + cq.dequeue()); // 출력: Dequeued: 10
System.out.println("Dequeued: " + cq.dequeue()); // 출력: Dequeued: 20
cq.display(); // 출력: 30 40 50
cq.enqueue(60);
cq.enqueue(70);
cq.display(); // 출력: 30 40 50 60 70
System.out.println("Front element: " + cq.peek()); // 출력: Front element: 30
}
}
🙋♂️ 설명.
큐 초기화:
‘capacity’ : 큐의 최대 크기입니다.
‘queue’ : 큐를 저장할 배열입니다.
‘front’ : 큐의 첫 번째 요소를 가리키는 인덱스입니다.
‘rear’ : 큐의 마지막 요소를 가리키는 인덱스입니다.
‘size’ : 큐에 있는 요소의 개수입니다.
메서드:
‘isEmpty()’ : 큐가 비어 있는지 확인합니다.
‘isFull()’ : 큐가 가득 찼는지 확인합니다.
‘enqueue(int element)’ : 큐에 요소를 추가합니다.
‘dequeue()’ : 큐에서 요소를 제거하고 반환합니다.
‘peek()’ : 큐의 첫 번째 요소를 반환합니다.
‘getSize()’ : 큐에 있는 요소의 개수를 반환합니다.
‘display()’ : 큐의 모든 요소를 출력합니다.
5️⃣ 결론.
원형 큐는 배열을 효율적으로 사용하여 큐의 크기를 고정하고, 처음과 끝이 연결된 형태로 큐를 관리하는 자료구조입니다.
이를 통해 큐의 공간을 최대한 활용하고, 큐가 비어 있는지 가득 찼는지를 쉽게 확인할 수 있습니다.
🤔 궁금했던 부분.
rear = (rear + 1) % capacity;
1️⃣ 이 코드에서 % capacity 를 하는 이유는 무엇일까?
원형 큐에서 ‘rear’ 포인터를 업데이트 할 때 % capacity 를 사용하는 이유는 큐가 마지막 인덱스에 도달한 후, 다시 처음 인덱스로 돌아가도록 하기 위해서입니다.
이를 통해 큐가 원형으로 동작할 수 있습니다.
구체적으로 말하면, 큐의 크기를 고정된 크기의 배열로 구현할 때, 배열의 끝에 도달했을 때 다시 처음으로 돌아가는 기능을 제공합니다.
2️⃣ % 연산자의 역할.
배열의 인덱스는 0부터 시작하여 배열의 크기보다 1 작은 값까지입니다.
예를 들어, 배열의 크기가 5라면 인덱스는 0부터 4까지입니다.
원형 큐에서 새로운 요소를 추가할 때마다 ‘rear’ 포인터를 증가시키는데, 이 포인터가 배열의 끝을 넘어가지 않도록 해야 합니다.
이를 위해 % capacity 연산을 사용합니다.
rear = (rear + 1) % capacity;
이 연산은 ‘rear’ 포인터를 1씩 증가시키다가, 배열의 끝에 도달하면 다시 0으로 돌아가게 합니다.
즉, 배열의 인덱스가 배열의 크기를 넘어가면, 다시 처음 인덱스(0)로 순환되게 합니다.
👉 예제.
배열의 크기가 5인 원형 큐를 생각해봅시다.
초기 상태: ‘rear = -1’
요소 추가 시, ‘rear’ 포인터의 변화를 관찰해보면
첫 번째 추가: ‘rear = (rear + 1) % 5 -> rear = 0’
두 번째 추가: ‘rear = (rear + 1) % 5 -> rear = 1’
세 번째 추가: ‘rear = (rear + 1) % 5 -> rear = 2’
네 번째 추가: ‘rear = (rear + 1) % 5 -> rear = 3’
다섯 번째 추가: ‘rear = (rear + 1) % 5 -> rear = 4’
여섯 번째 추가: ‘rear = (rear + 1) % 5 -> rear = 0’ (다시 처음으로 돌아감)
이렇게 ‘rear’ 포인터가 배열의 끝에 도달하면 다시 배열의 시작 부분으로 순환되므로, 배열을 효율적으로 사용할 수 있게 됩니다.
💻 코드 예제.
위 개념을 이용한 원형 큐의 ‘enqueue’ 메서드 구현
public void enqueue(int element) {
if (isFull()) {
System.out.println("Queue is full");
return;
}
rear = (rear + 1) % capacity; // rear 포인터를 증가시키고, 배열의 처음으로 순환시킴.
queue[rear] = element;
size++;
}
6️⃣ 정리.
원형 큐에서 ’% capacity’ 연산은 ‘rear’ 포인터와 ‘front’ 포인터가 배열의 끝에 도달했을 때, 다시 배열의 시작 부분으로 돌아가기 위해 사용됩니다.
이를 통해 배열의 고정된 크기를 효율적으로 활용하며, 원형 큐의 특성을 유지할 수 있습니다.
-
-
-
☕️[Java] 제네릭(Generic)
1️⃣ 제네릭(Generic)
Java에서의 제네릭(Generic) 은 클래스나 메서드에서 사용할 데이터 타입을 나중에 지정할 수 있도록 하는 기능입니다.
제네릭을 사용하면 코드의 재사용성을 높이고, 컴파일 시 타입 안전성을 제공하며, 명시적 타입 캐스팅을 줄일 수 있습니다.
2️⃣ 제네릭(Generic)의 주요 개념.
타입 매개변수 :
제네릭 클래스나 메서드는 타입 매개변수를 사용하여 타입을 정의합니다. 이 타입 매개변수는 클래스나 메서드가 호출될 때 구체적인 타입으로 대체됩니다.
타입 안정성 :
제네릭을 사용하면 컴파일 시 타입을 검사하므로, 런타입에 발생할 수 있는 타입 오류를 줄일 수 있습니다.
재사용성 :
제네릭 클래스나 메서드는 다양한 타입에 대해 동작하도록 설계할 수 있어, 코드의 재사용성을 높입니다.
3️⃣ 제네릭 클래스.
제네릭 클래스는 클래스 선언에 타입 매개변수를 포함하여 정의합니다.
일반적으로 타입 매개변수는 한 글자로 표현 되며, ‘T(Tyep)‘, ‘E(Element)‘, ‘K(Key)‘, ‘V(Value)‘ 등이 자주 사용됩니다.
예제.
// Box 클래스
public class Box<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
// Main 클래스
public class Main {
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.setItem("Hello");
System.out.println("String item: " + stringBox.getItem()); // String item: Hello
Box<Integer> integerBox = new Box<>();
integerBox.setItem(123);
System.out.println("Integer item: " + integerBox.getItem()); // Integer item: 123
}
}
4️⃣ 제네릭 메서드.
제네릭 메서드는 메서드 선언 타입 매개변수를 포함하여 정의합니다.
예제.
public class GenericMethodExample {
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
String[] strArray = {"A", "B", "C", "D"};
printArray(intArray); // 1 2 3 4 5
printArray(strArray); // A B C D
}
}
5️⃣ 제네릭 타입 제한 (Bounded Type Parameters)
제네릭 타입 매개변수에 제한을 걸어 특정 타입의 하위 클래스나 인터페이스만 허용할 수 있습니다.
상한 제한 (Upper Bound)
public class BoundedTypeExample<T extends Number> {
private T number;
public BoundedTypeExample(T number) {
this.number = number;
}
public void printNumber() {
System.out.println("Number: " + number);
}
public static void main(String[] args) {
BoundedTypeExample<Integer> intExample = new BoundedTypeExample<>(123);
intExample.printNumber(); // Number: 123
BoundedTypeExample<Double> doubleExample = new BoundedTypeExample<>(45.67);
doubleExample.printNumber(); // Number: 45.67
}
}
여기서 ‘T’ 는 ‘Number’ 클래스나 그 하위 클래스만 될 수 있습니다.
하한 제한 (Lower Bound)
하한 제한은 와일드카드(’? super T‘)를 사용하여 정의됩니다.
예를 들어 ‘List<? super Integer>‘ 는 ‘Integer‘ 의 상위 타입인 ‘Number‘, ‘Object‘ 등이 될 수 있습니다.
import java.util.ArrayList;
import java.util.List;
public class LowerBoundWildcardExample {
public static void addNumbers(List<? super Integer> list) {
for (int i = 0; i < 5; i++) {
list.add(i);
}
}
public static void main(String[] args) {
List<Number> numberList = new ArrayList<>();
addNumbers(numberList);
System.out.println(numberList); // [0, 1, 2, 3, 4]
}
}
6️⃣ 제네릭의 제한 사항.
Primitive Type 사용 불가 : 제네릭은 참조 타입만 허용하며, 기본 타입은 사용할 수 없습니다.
// 올바르지 않음
Box<int> intBox = new Box<>(); // 컴파일 오류
정적 컨텍스트에서의 타입 매개변수 사용 : 정적 메서드나 정적 변수에서는 타입 매개변수를 사용할 수 없습니다.
public class GenericClass<T> {
private static T item; // 컴파일 오류
}
제네릭 배열 생성 불가 : 제네릭 배열을 직접 생성할 수 없습니다.
// 올바르지 않음
T[] array = new T[10]; // 컴파일 오류
제네릭은 Java의 강력한 기능으로, 타입 안전성을 높이고 코드의 재사용성을 극대화할 수 있게 해줍니다.
이를 적절히 활용하면 더 안정적이고 유지보수하기 쉬운 코드를 작성할 수 있습니다.
-
-
📦[DS,Algorithm] LinkedList를 사용한 Deque.
1️⃣ LinkedList를 사용한 Deque.
‘LinkedList‘ 는 ‘Deque‘ 인터페이스를 구현한 클래스 중 하나로, 양쪽 끝에서 삽입과 삭제가 가능한 이중 연결 리스트 기반의 자료 구조입니다.
‘LinkedList‘ 는 ‘Deque‘ 뿐만 아니라 ‘List‘, ‘Queue‘ 인터페이스도 구현하여 다양한 형태로 사용할 수 있습니다.
2️⃣ 주요 특징.
이중 끝 큐 : 양쪽 끝에서 요소를 추가하고 제거할 수 있습니다.
이중 연결 리스트 : 각 노드는 이전 노드와 다음 노드를 가리키는 두 개의 포인터를 가집니다.
비동기적 : ‘LinkedList‘ 는 비동기적으로 동작하므로 동기화된 환경에서 안전하지 않습니다.
3️⃣ 주요 메서드.
삽입 연산.
‘addFirst(E e)’ : 지정된 요소를 덱의 앞쪽에 추가합니다.
‘addLast(E e)’ : 지정된 요소를 덱의 뒤쪽에 추가합니다.
‘offerFirst(E e)’ : 지정된 요소를 덱의 앞쪽에 추가합니다.
‘offerLast(E e)’ : 지정된 요소를 덱의 뒤쪽에 추가합니다.
삭제 연산.
‘removeFirst()’ : 덱의 앞쪽에서 요소를 제거하고 반환합니다.
‘removeLast()’ : 덱의 뒤쪽에서 요소를 제거하고 반환합니다.
‘pollFirst()’ : 덱의 앞쪽에서 요소를 제거하고 반환합니다.
‘pollLast()’ : 덱의 뒤쪽에서 요소를 제거하고 반환합니다.
조회 연산.
‘getFirst()’ : 덱의 앞쪽에 있는 요소를 반환합니다.
‘getLast()’ : 덱의 뒤쪽에 있는 요소를 반환합니다.
‘peekFirst()’ : 덱의 앞쪽에 있는 요소를 반환합니다.
‘peekLast()’ : 덱의 뒤쪽에 있는 요소를 반환합니다.
스택 연산.
‘push(E e)’ : 스택의 맨 위에 요소를 추가합니다.(FIFO, First In First Out)
‘pop()’ : 스택의 맨 위에 있는 요소를 제거하고 반환합니다.(LIFO, Last In First Out)
4️⃣ 시간 복잡도
삽입과 삭제 연산 : ‘addFirst‘, ‘addLast‘, ‘removeFirst‘, ‘removeLast‘, ‘offerFirst‘, ‘offerLast‘, ‘pollFirst‘, ‘pollLast‘ 등의 연산은 O(1)입니다.
이중 연결 리스트를 사용하기 때문에 양쪽 끝에서의 삽입과 삭제는 상수 시간 내에 수행됩니다.
조회 연산 : ‘getFirst‘, ‘getLast‘, ‘peekFirst‘, ‘peekLast‘ 등의 연산은 O(1)입니다.
임의 접근 연산( **‘get(int index)‘, ‘set(int index, E element)’ 등) :** 인덱스를 사용한 접근 연산은 리스트의 중간에 있는 요소를 찾기 위해 리스트를 순회해야 하므로 O(n) 시간이 걸립니다.
5️⃣ 코드 예시.
아래 코드는 ‘LinkedList‘ 를 ‘Deque‘ 로 사용하는 예제입니다.
import java.util.Deque;
import java.util.LinkedList;
public class LinkedListDequeExample {
public static void main(String[] args) {
// LinkedList로 Deque 생성
Deque<Integer> deque = new LinkedList<>();
// 요소 삽입
deque.addFirst(1);
deque.addLast(2);
deque.offerFirst(0);
deque.offerLast(3);
// 요소 조회
System.out.println("First element: " + deque.getFirst());
System.out.println("Last element: " + deque.getLast());
System.out.println("Peek first element: " + deque.peekFirst());
System.out.println("Peek last element: " + deque.peekLast());
// 요소 식제
System.out.println("Removed first element: " + deque.removeFirst());
System.out.println("Removed last element: " + deque.removeLast());
System.out.println("Poll first element: " + deque.pollFirst());
System.out.println("Poll last element: " + deque.pollLast());
// 덱의 크기와 비어 있는지 여부 확인
System.out.println("Deque size: " + deque.size());
System.out.println("Is deque empty? " + deque.isEmpty());
// 스택 연산.
deque.push(4);
System.out.println("Pushed element: " + deque.peekFirst());
System.out.println("Popped element: " + deque.pop());
}
}
/*
=== 출력 ===
First element: 0
Last element: 3
Peek first element: 0
Peek last element: 3
Removed first element: 0
Removed last element: 3
Poll first element: 1
Poll last element: 2
Deque size: 0
Is deque empty? true
Pushed element: 4
Popped element: 4
*/
🙋♂️ 설명.
베열 초기화 : ‘DEFAULT_CAPACITY‘ 크기의 배열을 초기화하고, ‘head‘, ‘tail‘, ‘size‘ 변수를 초기화 합니다.
삽입 연산( **‘addFirst‘, ‘addLast‘) :** 요소를 덱의 첫 번째 또는 마지막에 추가합니다.
삭제 연산( **‘removeFirst‘, ‘removeLast‘) :** 첫 번째 요소와 마지막 요소를 각각 제거합니다.
조회 연산( **‘getFirst‘, ‘getLast‘, ‘peekFirst‘, ‘peekLast‘) :** 첫 번째 요소와 마지막 요소를 반환합니다.
기타 메서드 : ‘size‘ 와 ‘isEmpty‘ 메서드는 덱의 크기와 비어 있는지 여부를 반환합니다.
스택 연산( **‘push‘, ‘pop‘) :** 스택의 맨 위에 요소를 추가하고, 스택의 맨 위에 있는 요소를 제거하고 반환합니다.
위 예시 코드에서는 ‘LinkedList‘ 를 ‘Deque‘ 로 사용하여 다양한 연산을 수행하는 방법을 보여줍니다.
‘LinkedList‘ 는 이중 연결 리스트를 사용하기 때문에 양쪽 끝에서의 삽입과 삭제가 빠르고 효율적입니다.
-
-
📦[DS,Algorithm] ArrayDeque
1️⃣ ArrayDeque.
Java에서 ‘ArrayDeque‘ 는 ‘java.util‘ 패키지에 속하는 클래스이며, 큐(Queue)와 덱(Deque)의 기능을 모두 지원하는 배열 기반의 자료 구조입니다.
‘ArrayDeque‘ 는 ‘Deque‘ 인터페이스를 구현하며, 그기가 가변적인 배열을 사용하여 요소를 저장합니다.
2️⃣ 주요 특징.
이중 끝 큐 : 양쪽 끝에서 요소를 추가하고 제거할 수 있습니다.
크기 조정 : 필요에 따라 내부 배열의 크기를 자동으로 조정합니다.
스택 및 큐로 사용 가능 : ‘ArrayDeque‘ 는 스택(LIFO, Last In First Out)과 큐(FIFO, First In First Out) 모두로 사용할 수 있습니다.
비동기적 : ‘ArrayDeque‘ 는 비동기적으로 동작하므로 동기화된 환경에서 안전하지 않습니다.
3️⃣ 주요 메서드.
삽입 연산.
‘addFirst(E e)’ : 지정된 요소를 덱의 앞쪽에 추가합니다.
‘addLast(E e)’ : 지정된 요소를 덱의 뒤쪽에 추가합니다.
‘offerFirst(E e)’ : 지정된 요소를 덱의 앞쪽에 추가합니다.
‘offerLast(E e)’ : 지정된 요소를 덱의 뒤쪽에 추가합니다.
삭제 연산.
‘removeFirst()’ : 덱의 앞쪽에서 요소를 제거하고 반환합니다.
‘removeLast()’ : 덱의 뒤쪽에서 요소를 제거하고 반환합니다.
‘pollFirst()’ : 덱의 앞쪽에서 요소를 제거하고 반환합니다.
‘pollLast()’ : 덱의 뒤쪽에서 요소를 제거하고 반환합니다.
조회 연산.
‘getFirst()’ : 덱의 앞쪽에 있는 요소를 반환합니다.
‘getLast()’ : 덱의 뒤쪽에 있는 요소를 반환합니다.
‘peekFirst()’ : 덱의 앞쪽에 있는 요소를 반환합니다.
‘peekLast()’ : 덱의 뒤쪽에 있는 요소를 반환합니다.
스택 연산.
‘push(E e)’ : 스택의 맨 위에 요소를 추가합니다.(LIFO, Last In First Out)
‘pop(E e)’ : 스택의 맨 위에 있는 요소를 제거하고 반환합니다.(LIFO, Last In First Out)
4️⃣ 시간 복잡도.
삽입과 삭제 연산 : ‘addFirst‘, ‘addLast‘, ‘removeFirst‘, ‘removeLast‘, ‘offerFirst‘, ‘offerLast‘, ‘pollFirst‘, ‘pollLast‘, 등의 연산은 평균적으로 O(1)입니다.
조회 연산 : ‘getFirst‘, ‘getLast‘, ‘peekFirst‘, ‘peekLast‘ 등의 연산은 O(1)입니다.
크기 조정 : 베열의 크기가 가득 찼을 때 크기를 두 배로 늘리거나 줄이는 작업은 O(n) 시간이 걸리지만, 이는 드물게 발생하므로 평균적으로는 O(1)로 간주합니다. (amortized O(1)).
5️⃣ 예제 코드
아래의 코드는 ‘ArrayDeque‘ 를 사용한 예제 코드입니다.
import java.util.ArrayDeque;
import java.util.Deque;
public class ArrayDequeExample {
public static void main(String[] args) {
// ArrayDeque로 Deque 생성
Deque<Integer> deque = new ArrayDeque<>();
// 요소 삽입
System.out.println("=== 요소 삽입 ===");
deque.addFirst(1);
deque.addLast(2);
deque.offerFirst(0);
deque.offerLast(3);
System.out.println(deque);
System.out.println();
// 요소 조회
System.out.println("=== 요소 조회 ===");
System.out.println("First element: " + deque.getFirst());
System.out.println("Last element: " + deque.getLast());
System.out.println("Peek first element: " + deque.peekFirst());
System.out.println("Peek last element: " + deque.peekLast());
System.out.println();
// 요소 삭제
System.out.println("=== 요소 삭제 ===");
System.out.println("Removed first element: " + deque.removeFirst());
System.out.println("Removed last element: " + deque.removeLast());
System.out.println("Poll first element: " + deque.pollFirst());
System.out.println("Poll last element: " + deque.pollLast());
System.out.println();
// 덱의 크기와 비어 있는지 여부 확인
System.out.println("=== 덱의 크기와 비어 있는지 여부 확인 ===");
System.out.println("Deque size: " + deque.size());
System.out.println("Is deque empty? " + deque.isEmpty());
System.out.println();
// 스택 연산
System.out.println("=== 스택 연산 ===");
deque.push(4);
System.out.println("Pushed element: " + deque.peekFirst());
System.out.println("Popped element: " + deque.pop());
}
}
/*
=== 출력 ===
=== 요소 삽입 ===
[0, 1, 2, 3]
=== 요소 조회 ===
First element: 0
Last element: 3
Peek first element: 0
Peek last element: 3
=== 요소 삭제 ===
Removed first element: 0
Removed last element: 3
Poll first element: 1
Poll last element: 2
=== 덱의 크기와 비어 있는지 여부 확인 ===
Deque size: 0
Is deque empty? true
=== 스택 연산 ===
Pushed element: 4
Popped element: 4
*/
-
-
📦[DS,Algorithm] Deque(데크, 덱)
1️⃣ Deque(덱, Double Ended Queue)
Deque(덱, Double Ended Queue)는 양쪽 끝에서 삽입과 삭제를 할 수 있는 자료 구조입니다.
Java에서는 java.util 패키지에서 제공하는 Deque 인터페이스와 이를 구현한 클래스인 ArrayDeque 와 LinkedList 를 통해 사용할 수 있습니다.
Deque 는 큐(Queue)와 스택(Stack)의 기능을 모두 포함하고 있습니다.
1️⃣ 데크 기본 구조
데크의 기본 구조는 양방향에서 삽입 삭제 가능한 구조
일부 기능을 제한하여 용도에 맞게 변형 가능
add나 remove 계열은 예외를 발생시킵니다.
때문에 예외 처리가 가능합니다.
offer이나 poll 계열은 null이나 false를 반환합니다.
때문에 return값 (반환값)을 받아서 처리할 수 있습니다.
2️⃣ Deque의 주요 메서드.
1️⃣ 삽입 연산.
addFirst(E e) : 지정된 요소를 덱의 앞쪽에 추가합니다.
addLast(E e) : 지정된 요소를 덱의 뒤쪽에 추가합니다.
offerFirst(E e) : 지정된 요소를 덱의 앞쪽에 추가합니다.
offerLast(E e) : 지정된 요소를 덱의 뒤쪽에 추가합니다.
2️⃣ 삭제 연산.
removeFirst() : 덱의 앞쪽에서 요소를 제거하고 반환합니다.
removeLast() : 덱의 뒤쪽에서 요소를 제거하고 반환합니다.
pollFirst() : 덱의 앞쪽에서 요소를 제거하고 반환합니다.
pollLast() : 덱의 뒤쪽에서 요소를 제거하고 반환합니다.
3️⃣ 조회 연산.
getFirst() : 덱의 앞쪽에 있는 요소를 반환합니다.
getLast() : 덱의 뒤쪽에 있는 요소를 반환합니다.
peekFirst() : 덱의 앞쪽에 있는 요소를 반환합니다.
peekLast() : 덱의 뒤쪽에 있는 요소를 반환합니다.
4️⃣ 기타 연산.
size() : 덱에 있는 요소의 수를 반환합니다.
isEmpty() : 덱이 비어 있는지 여부를 확인합니다.
3️⃣ 시간 복잡도.
Deque 인터페이스의 시간 복잡도는 이를 구현한 클래스에 따라 달라집니다.
Java에서는 주로 ArrayDeque 와 LinkedList 를 사용하여 Deque 를 구현합니다.
1️⃣ ArrayDeque
삽입과 삭제 연산 (앞과 뒤 모두): 평균적으로 O(1)
조회 연산 (앞과 뒤 모두): O(1)
ArrayDeque 는 배열을 기반으로 구현되기 때문에, 배열이 꽉 차면 자동으로 크기를 늘리지만, 이 과정은 amortized O(1)로 간주됩니다.
2️⃣ LinkedList
삽입과 삭제 연산 (앞과 뒤 모두): O(1)
조회 연산 (앞과 뒤 모두): O(1)
LinkedList 는 이중 연결 리스트로 구현되어 있어 각 노드가 이전과 다음 노드에 대한 참조를 가지고 있습니다.
LinkedList는 각 노드가 이전 노드와 다음 노드의 참조를 가지고 있어 삽입과 삭제가 O(1)의 시간 복잡도를 가집니다.
하지만 탐색에는 O(n)의 시간이 소요됩니다.
ArrayDeque는 배열을 사용하여 내부적으로 구현되기 때문에 삽입과 삭제 시에도 평균적으로 O(1)의 시간 복잡도를 가지며,
특히 큐의 끝에서의 연산이 빠릅니다.
다만, 내부적으로 배열이 가득 차면 크기를 조정해야 하므로 최악의 경우 O(n)의 시간 복잡도가 발생할 수 있습니다.
Deque 는 다양한 상황에서 유연하게 사용될 수 있는 유용한 자료구조입니다.
특히 양쪽 끝에서의 빠른 삽입과 삭제가 필요한 경우 유용합니다.
3️⃣ 직접 Deque 인터페이스 구현.
간단한 배열을 사용하여 Deque 를 구현해보겠습니다.
import java.util.ArrayList;
import java.util.NoSuchElementException;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class SimpleArrayDeque<E> {
private static final int DEFALT_CAPACITY = 10;
private E[] elements;
private int head;
private int tail;
private int size;
public SimpleArrayDeque() {
elements = (E[]) new Object[DEFALT_CAPACITY];
head = 0;
tail = 0;
size = 0;
}
public void addFirst(E e) {
if (size == elements.length) {
resize();
}
head = (head - 1 + elements.length) % elements.length;
elements[head] = e;
size++;
}
public void addLast(E e) {
if (size == elements.length) {
resize();
}
elements[tail] = e;
tail = (tail + 1) % elements.length;
size++;
}
public E removeFirst() {
if (size == 0) {
throw new NoSuchElementException();
}
E element = elements[head];
elements[head] = null; // for garbege collection
head = (head + 1);
size--;
return element;
}
public E removeLast() {
if (size == 0) {
throw new NoSuchElementException();
}
tail = (tail - 1 + elements.length) % elements.length;
E element = elements[tail];
elements[tail] = null; // for garbage collection
size--;
return element;
}
public E getFirst() {
if (size == 0) {
throw new NoSuchElementException();
}
return elements[head];
}
public E getLast() {
if (size == 0) {
throw new NoSuchElementException();
}
return elements[(tail - 1 + elements.length) % elements.length];
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
private void resize() {
int newCapacity = elements.length * 2;
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[(head + i) % elements.length];
}
elements = newElements;
head = 0;
tail = size;
}
public ArrayList<E> toArrayList() {
return IntStream.range(0, size)
.mapToObj(i -> elements[(head + i) % elements.length])
.collect(Collectors.toCollection(ArrayList::new));
}
}
// Main
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
SimpleArrayDeque<Integer> deque = new SimpleArrayDeque<>();
deque.addFirst(1);
deque.addLast(2);
deque.addFirst(0);
deque.addLast(3);
ArrayList<Integer> dequeList = deque.toArrayList();
System.out.println("=== dequeList === ");
System.out.println(dequeList);
System.out.println("First element: " + deque.getFirst());
System.out.println("Last element: " + deque.getLast());
System.out.println("=== dequeList === ");
dequeList = deque.toArrayList();
System.out.println(dequeList);
System.out.println("Removed first element: " + deque.removeFirst());
System.out.println("Remove last element: " + deque.removeLast());
System.out.println("=== dequeList === ");
dequeList = deque.toArrayList();
System.out.println(dequeList);
System.out.println("Deque size: " + deque.size());
System.out.println("Is deque empty? " + deque.isEmpty());
System.out.println("=== dequeList === ");
dequeList = deque.toArrayList();
System.out.println(dequeList);
}
}
/*
=== 출력 ===
=== dequeList ===
[0, 1, 2, 3]
First element: 0
Last element: 3
=== dequeList ===
[0, 1, 2, 3]
Removed first element: 0
Remove last element: 3
=== dequeList ===
[1, 2]
Deque size: 2
Is deque empty? false
=== dequeList ===
[1, 2]
*/
4️⃣ 입력 제한 Deque(Input-Restricted Deque).
입력 제한 Deque(Input-Restricted Deque)은 덱의 한쪽 끝에서만 삽입이 가능하고, 양쪽 끝에서 삭제가 가능한 자료구조입니다.
import java.util.ArrayList;
import java.util.NoSuchElementException;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class InputRestrictedDeque<E> {
private static final int DEFAULT_CAPACITY = 10;
private E[] elements;
private int head;
private int tail;
private int size;
@SuppressWarnings("unchecked")
public InputRestrictedDeque() {
elements = (E[]) new Object[DEFAULT_CAPACITY];
head = 0;
tail = 0;
size = 0;
}
public void addLast(E e) {
if (size == elements.length) {
resize();
}
elements[tail] = e;
tail = (tail + 1) % elements.length;
size++;
}
public E removeFirst() {
if (size == 0) {
throw new NoSuchElementException();
}
E element = elements[head];
elements[head] = null; // for garbage collection
head = (head + 1) % elements.length;
size--;
return element;
}
public E removeLast() {
if (size == 0) {
throw new NoSuchElementException();
}
tail = (tail - 1 + elements.length) % elements.length;
E element = elements[tail];
elements[tail] = null; // for gatbage collection
size--;
return element;
}
public E getFirst() {
if (size == 0) {
throw new NoSuchElementException();
}
return elements[head];
}
public E getLast() {
if (size == 0) {
throw new NoSuchElementException();
}
return elements[(tail - 1 + elements.length) % elements.length];
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
private void resize() {
int newCapacity = elements.length * 2;
@SuppressWarnings("unchecked")
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[(head + i) % elements.length];
}
elements = newElements;
head = 0;
tail = size;
}
public ArrayList<E> toArrayList() {
return IntStream.range(0, size)
.mapToObj(i -> elements[(head + i) % elements.length])
.collect(Collectors.toCollection(ArrayList::new));
}
}
// Main
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
InputRestrictedDeque<Integer> deque = new InputRestrictedDeque<>();
deque.addLast(1);
deque.addLast(2);
deque.addLast(3);
ArrayList<Integer> dequeList = deque.toArrayList();
System.out.println("=== dequeList ===");
System.out.println(dequeList);
System.out.println("First element: " + deque.getFirst());
System.out.println("Last element: " + deque.getLast());
System.out.println("=== dequeList ===");
dequeList = deque.toArrayList();
System.out.println(dequeList);
System.out.println("Remove first element: " + deque.removeFirst());
System.out.println("Remove last elment: " + deque.removeLast());
System.out.println("=== dequeList ===");
dequeList = deque.toArrayList();
System.out.println(dequeList);
System.out.println("Deque size: " + deque.size());
System.out.println("Is deque empty? " + deque.isEmpty());
}
}
/*
=== 출력 ===
=== dequeList ===
[1, 2, 3]
First element: 1
Last element: 3
=== dequeList ===
[1, 2, 3]
Remove first element: 1
Remove last elment: 3
=== dequeList ===
[2]
Deque size: 1
Is deque empty? false
*/
1️⃣ 코드 설명.
배열 초기화 : DEFAULT_CAPACITY 크기의 배열을 초기화하고, head, tail, size 변수를 초기화합니다.
삽입 연산(addLast) : 요소를 덱의 마지막 에 추가합니다. 배열이 가득 차면 크기를 두 배로 늘립니다.
삭제 연산(removeFirst, removeLaste) : 첫 번째 요소와 마지막 요소를 각각 제거합니다.
조회 연산(getFirst, getLast) : 첫 번째 요소와 마지막 요소를 반환합니다.
기타 메서드 : size 와 isEmpty 메서드는 덱의 크기와 덱이 비어 있는지 여부를 반환합니다.
배열 크기 조정 (resize) : 배열이 가득 찰 때 호출되며, 배열의 크기를 두 배로 늘리고 요소를 새 배열로 복사합니다.
이 예제에서는 요소를 덱의 끝에만 삽입할 수 있는 입력 제한 덱을 구현했습니다.
필요에 따라 이 구현을 확장하거나 수정하여 요구사항에 맞게 사용할 수 있습니다.
5️⃣ 출력 제한 Deque(Output-Restricted Deque).
출력 제한 Deque(Output-Restricted Deque)은 양쪽 끝에서 삽입이 가능하지만, 한쪽 끝에서만 삭제가 가능한 자료 구조입니다.
이 구조는 양쪽 끝에서 요소를 추가할 수 있지만, 삭제는 한쪽 끝에서만 할 수 있습니다.
import java.util.ArrayList;
import java.util.NoSuchElementException;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class OutputRestrictedDeque<E> {
private static final int DEFAULT_CAPACITY = 10;
private E[] elements;
private int head;
private int tail;
private int size;
@SuppressWarnings("unchecked")
public OutputRestrictedDeque() {
elements = (E[]) new Object[DEFAULT_CAPACITY];
head = 0;
tail = 0;
size = 0;
}
public void addFirst(E e) {
if (size == elements.length) {
resize();
}
head = (head - 1 + elements.length) % elements.length;
elements[head] = e;
size++;
}
public void addLast(E e) {
if (size == elements.length) {
resize();
}
elements[tail] = e;
tail = (tail + 1) % elements.length;
size++;
}
public E removeFirst() {
if (size == 0) {
throw new NoSuchElementException();
}
E element = elements[head];;
elements[head] = null; // for garbage collection
head = (head + 1) % elements.length;
size--;
return element;
}
public E getFirst() {
if (size == 0) {
throw new NoSuchElementException();
}
return elements[head];
}
public E getLast() {
if (size == 0) {
throw new NoSuchElementException();
}
return elements[(tail - 1 + elements.length) % elements.length];
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
private void resize() {
int newCapacity = elements.length * 2;
@SuppressWarnings("unchecked")
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[(head + 1) % elements.length];
}
elements = newElements;
head = 0;
tail = size;
}
public ArrayList<E> toArrayList() {
return IntStream.range(0, size)
.mapToObj(i -> elements[(head + i) % elements.length])
.collect(Collectors.toCollection(ArrayList::new));
}
}
// Main
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
OutputRestrictedDeque<Integer> deque = new OutputRestrictedDeque<>();
deque.addFirst(1);
deque.addLast(2);
deque.addFirst(0);
deque.addLast(3);
ArrayList<Integer> dequeList = deque.toArrayList();
System.out.println("=== dequeList === ");
System.out.println(dequeList);
System.out.println("First element: " + deque.getFirst());
System.out.println("Last element: " + deque.getLast());
System.out.println("=== dequeList === ");
dequeList = deque.toArrayList();
System.out.println(dequeList);
System.out.println("Remove first element: " + deque.removeFirst());
System.out.println("=== dequeList === ");
dequeList = deque.toArrayList();
System.out.println(dequeList);
System.out.println("Deque size: " + deque.size());
System.out.println("Is deque empty? " + deque.isEmpty());
System.out.println("=== dequeList === ");
dequeList = deque.toArrayList();
System.out.println(dequeList);
}
}
/*
=== 출력 ===
=== dequeList ===
[0, 1, 2, 3]
First element: 0
Last element: 3
=== dequeList ===
[0, 1, 2, 3]
Remove first element: 0
=== dequeList ===
[1, 2, 3]
Deque size: 3
Is deque empty? false
=== dequeList ===
[1, 2, 3]
*/
1️⃣ 코드 설명.
배열 초기화 : DEFAULT_CAPACITY 크기의 배열을 초기화하고, head, tail, size 변수를 초기화 합니다.
삽입 연산(addFirst, addLast) : 요소를 덱의 첫 번째 또는 마지막에 추가합니다. 배열이 가득 차면 크기를 두 배로 늘립니다.
삭제 연산(removeFirst) : 첫 번째 요소를 제거합니다. 출력 제한 덱에서는 첫 번째 요소만 제거할 수 있습니다.
조회 연산(getFirst, getLast) : 첫 번째 요소와 마지막 요소를 반환합니다.
기타 메서드 : size 와 isEmpty 메서드는 덱의 크기와 덱이 비어 있는지 여부를 반환합니다.
배열 크기 조정(resize) : 배열이 가득 찰 때 호출되며, 배열의 크기를 두 배로 늘리고 요소를 새 배열로 복사합니다.
이 예제에서는 요소를 덱의 양쪽 끝에서 삽입할 수 있고, 첫 번째 요소만 제거할 수 있는 출력 제한 덱을 구현했습니다.
필요에 따라 이 구현을 확장하거나 수정하여 요구사항에 맞게 사용할 수 있습니다.
-
-
☕️[Java] IntStream
1️⃣ Java Docs - IntStream.
Module : java.base
Package : java.util.stream
Interface IntStream
All SuperInterfaces : AutoCloseble, BaseStream<Integer, IntStream>
AutoCloseble
BaseStream
Integer
IntStream
public interface IntStream extends BaseStream<Integer, IntStream>
순차 및 병렬 집계 연산을 지원하는 기본 int 값 요소의 시퀀스입니다. 이것은 Stream의 int 기본형 특수화입니다.
IntStream 이 Stream 의 한 형태로, int 값의 시퀀스를 처리하며 순차 및 병렬 연산을 지원한다는 의미입니다.
다음 예제는 Stream과 IntStream을 사용하여 빨간색 위젯의 무게 합계를 계산하는 집계 연산을 보여줍니다.
int sum = widgets.stream()
.filter(w -> w.getColor() == RED)
.mapToInt(w -> w.getWeight())
.sum();
streams(스트림), stream operations(스트림 연산), stream pipelines(스트림 파이프라인), and parallelism(및 병렬 처리)에 대한 추가적인 명세는 Stream 클래스 문서와 java.util.stream 패키지 문서를 참조하십시오.
Since : 1.8
Nested Class Summary
Nested Classes
Modifier and Type: static interface
Interface: IntStream.Builder
Description: IntStream용 변경 가능한 빌더입니다.
2️⃣ IntStream.
IntStream 은 Java의 스트림 API(Stream API)의 일부로, 기본형 int 에 특화된 스트림을 나타냅니다.
IntStream 은 Java 8에서 도입된 스트림 API의 일부로, 컬렉션(리스트, 배열 등)과 같은 데이터 소스를 함수형 프로그래밍 스타일로 처리할 수 있게 해줍니다.
IntStream 은 Stream<Integer> 와는 달리 오토박싱과 언박싱의 오버헤드가 없는 것이 특징입니다.
🙋♂️ IntStream의 주요 기능
1. 생성:
IntStream 을 생성하는 방법은 여러가지가 있습니다.
예를 들어, 배열, 범위, 임의의 수 등을 사용하여 생성할 수 있습니다.
2. 연산:
스트림 연산은 두 가지로 나뉩니다.
중간 연산과 최종 연산.
중간 연산은 또 다른 스트림을 반환하고, 지연(lazy) 평가됩니다.
최종 연산은 스트림을 소비하여 결과를 반환합니다.
🙋♂️ IntStream 생성 방법.
1. of() 메서드:
고정된 개수의 int 값을 스트림으로 생성합니다.
IntStream stream = IntStream.of(1, 2, 3, 4, 5);
2. range() 및 rangeClosed() 메서드:
범위를 지정하여 스트림을 생성합니다. range 는 시작 값 포함, 끝 값 미포함, rangeClosed 는 시작 값과 끝 값을 모두 포함합니다.
IntStream stream = IntStream.range(0, 5); // 0, 1, 2, 3, 4, 5
IntStream closedStream = IntStream.rangeClosed(0, 5); // 0, 1, 2, 3, 4, 5
3. generate() 메서드:
람다 표현식을 사용하여 무한 스트림을 생성합니다.
🚨 주의: 무한 스트림은 반드시 제한을 걸아야 합니다.
IntStream stream = IntStream.generate(() -> 1).limit(5); // 1, 1, 1, 1, 1
4. iterate() 메서드:
초기값과 반복 함수로 스트림을 생성합니다.
IntStream stream = IntStream.iterate(0, n -> n + 2).limit(5); // 0, 2, 4, 6, 8
5. builder() 메서드:
IntStream.Builder 를 사용하여 스트림을 생성합니다.
IntStream.Builder builder = IntStream.builder()l
builder.add(1).add(2).add(3).add(4).add(5);
IntStream stream = builder.builder();
6. 배열에서 생성:
배열을 스트림으로 변환합니다.
int[] array = {1, 2, 3, 4, 5};
IntStream stream = Arrays.stream(array);
🙋♂️ IntStream의 주요 메서드.
1. 중간 연산:
map() : 각 요소에 함수 적용.
filter() : 조건에 맞는 요소만 통과
distinct() : 중복 요소 제거
sorted() : 정렬
limit() : 스트림 크기 제한
skip() : 처음 n개 요소 건너뛰기
2. 최종 연산:
forEach() : 각 요소에 대해 액션 수행
toArray() : 배열로 변환
reduce() : 모든 요소를 누적하여 하나의 값으로
collect() : 컬렉션으로 변환
sum() : 합계 연산
average() : 평균 계산
min(), max() : 최소, 최대값 찾기
count() : 요소 개수 반환
💻 예제 코드
예제 1: 0에서 5까지 거꾸로 출력.
import java.util.stream.IntStream;
public class Reverse {
public static void main(String[] args) {
IntStream.rangeClosed(0, 5)
.map(i -> 5 - i)
.forEach(System.out::println);
}
}
/*
=== 출력 ===
5
4
3
2
1
0
*/
예제 2: 배열의 합계 계산
import java.util.stream.IntStream;
public class ArraySum {
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5};
int sum = IntStream.of(array).sum();
System.out.println("sum = " + sum); // sum = 15
}
}
예제 3: 짝수 필터링
import java.util.stream.IntStream;
public class FilterEvenNumber {
public static void main(String[] args) {
IntStream.rangeClosed(1, 10)
.filter(n -> n % 2 == 0)
.forEach(System.out::println);
}
}
/*
=== 출력 ===
2
4
6
8
10
*/
📝 요약
IntStream 은 Java의 스트림 API의 일부분으로, 기본형 int에 특화된 스트림입니다.
이를 통해 컬렉션이나 배열을 함수형 프로그래밍 스타일로 처리할 수 있습니다.
IntStream 은 다양한 생성 방법과 중간 및 최종 연산을 제공하여 효율적이고 직관적인 데이터 처리를 가능하게 합니다.
📚 참고 문헌.
Java Docs - IntStream
-
-
📝[blog post] Java Docs 보는 방법.
📝 Java Docs를 읽는 능력이 필요한 이유. :)
저는 Documentation이 그 어떤 유명 테크 블로거의 글 보다 중요하고 심도있게 읽어야 한다는 개인적인 의견이 있습니다.
그 이유는 Java를 개발한 개발자분들이 직접 만든 설명서나 다름 없기 때문입니다.
우리가 레고를 생각해 봅시다.
내가 좋아하는 레고를 사서 집에서 조립할 때 무엇을 보나요? 🤔
맞습니다!
레고 패키지 안에 들어있는 “설명서”를 기반으로 레고를 조립합니다.
레고를 디자인하고 만드신 분이 직접 “이렇게 순서대로 만들면 당신이 원하는 멋진 레고 완성품을 얻을 수 있습니다!” 라는 것을 직.간접적으로 보여주는 아주 자세한 설명이 들어있죠 📝
설명서는 직접 디자인하고 설계한 사람의 철학과 그들이 왜 그렇게 만들었는지 그리고 어떻게 쓰여야하는지 정확, 명료하게 명시되어 있습니다.
또한 다른 구성품과 맞춰볼 수 있는 것도 제안하거나 보여주기도 합니다.
그래서 Documentation을 보고 제대로 활용할 줄 아는 것이 개발자에게는 중요한 능력 중 하나가 아닐까 하는 생각을 합니다 🙋♂️
1️⃣ Java Documentation 보기.
1. 온라인 문서.
Java SE Documentation은 Oracle 공식 사이트에서 제공됩니다.
Java 버전에 따라 다른 문서가 제공되니, 사용하는 Java 버전에 맞는 문서를 선택해야 합니다.
2. IDE 내장 문서.
많은 통합 개발 환경(IDE)에는 JavaDoc을 쉽게 볼 수 있는 기능이 내장되어 있습니다. InteillJ IDEA, Eclipes, NetBeans 등에서 코드 작성 시 JavaDocs를 볼 수 있습니다.
예를 들어, IntelliJ IDEA에서 클래스나 메소드 이름 위에 커서를 올리면 해당 클래스나 메소드의 JavaDoc이 팝업으로 표시됩니다.
3. 로컬 문서.
Java JDK를 설치할 때, JavaDoc을 로컬에 다운로드할 수 있습니다. 이를 통해 인터넷 연결 없이도 문서를 참조할 수 있습니다.
JDK 설치 경로 아래의 docs 폴더에 HTML 형식의 문서가 저장되어 있습니다.
2️⃣ Java Documentation 활용 방법
Java Documentation을 효과적으로 활용하는 방법을 알아봅시다.🤩
1. 클래스 및 메소드 탐색.
API 문서에서 패키지, 클래스, 메소드, 필드 등의 세부 정보를 탐색할 수 있습니다.
예를 들어, java.util 패키지에 어떤 클래스가 포함되어 있는지, ArrayList 클래스에 어떤 메소드가 있는지 등을 확인할 수 있습니다.
2. 사용 예제 찾기.
각 클래스와 메소드에는 사용 예제가 포함되어 있을 수 있습니다. 이러한 예제는 해당 API를 올바르게 사용하는 방법을 이해하는 데 도움이 됩니다.
3. 메소드 시그니처 및 설명.
메소드의 매개변수, 반환값, 예외 등을 설명하는 시그니처와 설명을 통해 메소드의 사용법을 정확히 알 수 있습니다.
예를 들어, String 클래스의 substring 메소드의 시그니처와 설명을 보면, 매개변수로 전달해야 할 값과 반환되는 값에 대한 정보를 얻을 수 있습니다.
4. 상속 구조 및 인터페이스.
클래스가 구현하는 인터페이스와 상속받는 클래스에 대한 정보를 확인할 수 있습니다. 이를 통해 클래스의 기능을 확장하거나 인터페이스를 구현하는 방법을 이해할 수 있습니다.
3️⃣ 예제
다음은 Java Documentation을 활용하는 몇 가지 예제입니다.
예제 1: ArrayList 클래스의 메소드 사용법 확인 🙋♂️
온라인 문서에서 ArrayList 클래스를 찾습니다.
Java SE Documentation에서 java.util.ArrayList 를 검색합니다.
ArrayList 클래스의 API 문서를 열어 메소드 목록을 확인합니다.
add(E e) 메소드 사용법 확인하기.
add(E e) 메소드는 리스트의 끝에 요소를 추가하는 메소드입니다.
메소드 설명을 읽고, 예제를 확인하여 사용법을 이해합니다.
예제 2. String 클래스의 substring 메소드 사용법 확인 🙋♂️
IDE 내장 문서 활용하기.
IntelliJ IDEA나 Eclipse에서 String 클래스의 substring 메소드를 사용하려고 할 때, 메소드 이름 위에 커서를 올리면 JavaDoc이 표시됩니다.
JavaDoc을 통해 substring(int beingIndex, int endIndex) 메소드의 매개변수와 반환 값에 대한 설명을 읽습니다.
public class Main {
public static void main(String[] args) {
String text = "Hello, World!";
String subText = text.substring(7, 12); // "World"
System.out.println(subText);
}
}
위 예제에서 substring 메소드의 매개변수가 beginIndex 와 endIndex 임을 알 수 있으며, 이는 시작 인덱스부터 종료 인덱스 전까지의 문자열을 반환합니다.
예제 3. 예외 처리 방법 확인 🙋♂️
예외 클래스 문서 확인하기.
java.lang.NullPointerException 클래스의 문서를 확인하여 언제 이 예외가 발생하는지, 그리고 이를 어떻게 처리할 수 있는지에 대한 정보를 얻습니다.
예외 처리 예제
public class Main {
public static void main(String[] args) {
try {
String text = null;
System.out.println(text.length());
} catch (NullPointerException e) {
System.out.println("Caught a NullPointerException");
}
}
}
이 예제는 NullPointException 이 발생할 때 이를 처리하는 방법을 보여줍니다.
📝 요약.
Java Documentation은 Java API를 이해하고 사용하는 데 필수적인 자료입니다.
Java Documentation를 온라인, IDE, 또는 로컬에서 접근할 수 있습니다.
API 문서를 통해 클래스와 메소드의 세부 정보를 확인하고, 예제를 참고하여 올바르게 사용하는 방법을 배울 수 있습니다.
상속 구조와 인터페이스 구현 방법을 이해하여 코드의 재사용성과 확장성을 높일 수 있습니다.
-
📦[DS,Algorithm] Java의 배열.
1️⃣ Java의 배열.
1️⃣ 배열이란 무엇인가?
배열(Array)은 동일한 타입의 여러 요소를 하나의 변수로 관리할 수 있게 해주는 자료구조입니다.
배열은 연속된 메모리 공간에 할당되며, 각 요소는 인덱스를 통해 접근할 수 있습니다.
2️⃣ 배열의 선언과 초기화.
Java에서 배열은 다음과 같이 선언하고 초기화할 수 있습니다.
int[] array = new int[5]; // 크기가 5인 정수형 배열 선언.
int[] array = {10, 20, 30, 40, 50}; // 초기화와 동시에 배열 선언
3️⃣ 배열의 요소와 접근.
배열의 각 요소는 인덱스를 통해 접근할 수 있으며, 인덱스는 0부터 시작합니다.
int firstElement = array[0]; // element = 10, 첫 번째 요소에 접근
array[1] = 25; // [10, 25, 30, 40, 50], 두 번째 요소에 값 25를 저장
4️⃣ 배열의 시간 복잡도.
배열의 시간 복잡도는 연산의 종류에 따라 다릅니다.
아래는 일반적인 배열 연산과 그 시간 복잡도를 설명한 것입니다.
1. 접근(Access)
특정 인덱스의 요소에 접근하는 시간 복잡도는 O(1)입니다.
이유 : 배열은 연속된 메모리 공간에 저장되므로 인덱스를 통해 바로 접근할 수 있기 때문입니다.
// 접근(Access)
int element = array[2];
// element = 30, time complexity = O(1)
// [10, 25, 30, 40, 50]
2. 탐색(Search)
배열에서 특정 값을 찾는 시간 복잡도는 O(n)입니다.
이유: 최악의 경우 배열의 모든 요소를 검사해야 할 수도 있기 때문입니다.
boolean found = false;
int target = 30;
for (int i = 0; i < array.length; i++) {
if (array[i] == target) { // i = 2, array[i] = 30
found = true;
break;
}
}
// [10, 25, 30, 40, 50]
3. 삽입(Insertion)
배열의 끝에 요소를 추가하는 시간 복잡도는 O(1)입니다.
배열의 특정 위치에 요소를 삽입하는 시간 복잡도는 O(n)입니다.
이유: 특정 위치에 삽입하기 위해서는 해당 위치 이후의 모든 요소를 한 칸씩 뒤로 밀어야 하기 때문입니다.
// 삽입(Insertion)
// 배열 삽입시 index가 array.length가 아니고 array.length - 1인 이유는
// array.length는 배열의 크기, 즉 5를 나타내기 때문입니다.
// index는 0부터 시작하기 때문에 배열의 크기가 5인 배열의 끝 index는 4입니다.
// 때문에 array.length - 1을 해줍니다.
array[array.length - 1] = 60; // 배열 끝에 삽입 (O(1)), [10, 25, 30, 40, 60]
// 배열 중간에 삽입하는 메서드
public static void insertion(int[] array, int index, int insertValue) {
// 배열 중간에 삽입(O(n))
for (int i = array.length - 1; i > index; i--) {
array[i] = array[i - 1];
}
array[index] = insertValue;
System.out.println(Arrays.toString(array));
}
4. 삭제(Deletion)
배열의 끝에서 요소를 제거하는 시간 복잡도는 O(1)입니다.
배열의 특정 위치의 요소를 제거하는 시간 복잡도는 O(n)입니다.
이유: 특정 위치의 요소를 제거한 후에는 해당 위치 이후의 모든 요소를 한 칸씩 앞으로 당겨야 하기 때문입니다.
// 삭제(Deletion)
array[array.length - 1] = 0; // 배열의 끝에서 삭제 ((O(1)), [10, 25, 30, 77, 0]
System.out.println(Arrays.toString(array));
// 배열 중간에서 삭제하는 메서드
int deletionValue = deletion(array, 2);
System.out.println(deletionValue); // 30
// 배열 중간에 삭제하는 메서드
public static int deletion(int[] array, int index) {
// 배열 중간에 삭제(O(n))
int[] returnValue = new int[array.length];
for (int i = index, j = 0; i < array.length - 1 ; i++) {
returnValue[j] = array[i];
j++;
array[i] = array[i + 1];
}
array[array.length - 1] = 0; // 마지막 요소 초기화.
int deletionValue = returnValue[0]; // 배열을 메모리에서 지우기
returnValue = null;
return deletionValue;
}
5️⃣ 배열의 장점과 단점.
장점.
빠른 접근 속도 : 인덱스를 통해 O(1) 시간에 요소를 접근할 수 있습니다.
메모리 효율 : 연속된 메모리 공간을 사용하므로 메모리 사용이 효율적입니다.
단점.
고정된 크기 : 배열의 크기는 선언 시에 고정되므로, 실행 중에 크기를 변경할 수 없습니다.
삽입 및 삭제의 비효율성 : 배열 중간에 요소를 삽입하거나 삭제할 때 O(n)의 시간이 소요됩니다.
연속된 메모리 할당 필요 : 큰 배열을 사용할 떄는 연속된 메모리 공간이 필요하여 메모리 할당에 제한이 있을 수 있습니다.
배열은 이러한 특성들로 인해 빠른 접근이 필요한 상황에서는 매우 유용하지만, 삽입 및 삭제가 빈번히 일어나는 경우에는 비효율적일 수 있습니다.
따라서 상황에 맞게 적절한 자료구조를 선택하는 것이 중요합니다.
-
-
📦[DS,Algorithm] 배열에서 특정 인덱스의 요소를 삭제하기.
1️⃣ 배열에서 특정 인덱스의 요소를 삭제하기.
Java에서 배열의 특정 인덱스의 요소를 삭제하는 방법은 배열의 구조 특성상 직접적으로 제공되지 않습니다.
때문에 일반적으로 요소를 삭제하기 위해 다음의 방법을 사용합니다.
2️⃣ 배열에서 요소를 삭제하는 방법 2가지.
1️⃣ 새로운 배열을 생성하여 요소를 복사하는 방법 :)
● 특정 인덱스의 요소를 건너뛰고 나머지 요소를 새로운 배열에 복사합니다.
방법 1 : 새로운 배열 생성하여 복사.
// 배열의 특정 인덱스의 요소를 삭제하는 방법 - 1
// 방법1. 새로운 배열을 생성하여 요소를 복사하는 방법
// - 특정 인덱스의 요소를 건너뛰고 나머지 요소를 새로운 배열에 복사합니다.
public class Main {
public static void main(String[] args) {
int[] array = {10, 20, 30, 40, 50};
array = removeElement(array, 0);
for (int value : array) {
System.out.println(value + " ");
}
}
// 특정 배열을 지우는 메소드
public static int[] removeElement(int[] array, int index) {
if (index < 0 || index >= array.length) {
throw new IndexOutOfBoundsException("Index out of bounds");
}
// 새로운 배열은 특정 요소를 지우기 때문에 기존 배열의 크기에서 -1 한 크기로 생성합니다.
int[] newArray = new int[array.length - 1];
for (int i = 0, j = 0; i < array.length; i++) {
if (i != index) {
newArray[j++] = array[i];
}
}
return newArray;
}
}
/*
=== 출력 ===
20
30
40
50
*/
“방법1의 장.단점”
새 배열 생성 : 메모리 사용량이 증가하지만, 원래 배열을 유지하고 싶은 경우 유용합니다.
2️⃣ 기존 배열을 이용하여 요소를 덮어쓰는 방법 :)
● 특정 인덱스 이후의 요소들을 앞으로 한 칸씩 이동시켜 덮어씁니다.
방법 2 : 기존 배열을 이용하여 요소 덮어쓰기.
// 배열의 특정 인덱스의 요소를 삭제하는 방법 - 2
// 방법2. 기존 배열을 이용하여 요소를 덮어쓰는 방법.
// - 특정 인덱스 이후의 요소들을 앞으로 한 칸씩 이동시켜 덮어 씁니다.
public class Main {
public static void main(String[] args) {
int[] array = {10, 20, 30, 40, 50};
array = removeElementInPlace(array, 0);
for (int value : array) {
System.out.println(value + " ");
}
}
public static int[] removeElementInPlace(int[] array, int index) {
if (index < 0 || index >= array.length) {
throw new IndexOutOfBoundsException("Index out of bounds");
}
for (int i = index; i < array.length - 1; i++) {
array[i] = array[i + 1];
}
// 배열의 마지막 요소를 0 또는 다른 기본값으로 설정 (선택 사항)
array[array.length - 1] = 0;
return array;
}
}
/*
=== 출력 ===
20
30
40
50
0
*/
“방법2의 장.단점”
기존 배열 사용 : 메모리를 절약할 수 있지만, 배열의 마지막 요소는 기본값으로 설정해야 합니다.
-
-
-
-
-
📦[DS,Algorithm] 스택(Stack)
1️⃣ 스택(Stack).
스택(Stack)은 자료구조의 한 종류로, 데이터가 일렬로 쌓이는 구조를 가지고 있습니다.
1️⃣ 스택(Stack)의 특징.
“후입선출(LIFO, Last In First Out)”로, 가장 나중에 삽입된 데이터가 가장 먼저 꺼내진다는 점이 특징입니다.
2️⃣ 스택(Stack)의 기본 연산.
푸시(Push) : 스택의 맨 위에 데이터를 삽입하는 연산.
팝(Pop) : 스택의 맨 위에 있는 데이터를 제거하고 반환하는 연산.
3️⃣ 스택(Stack)의 부가적인 연산.
피크(peek) 또는 탑(top) : 스택의 맨 위에 있는 데이터를 제거하지 않고 반환하는 연산.
isEmpty : 스택이 비어 있는지 여부를 확인하는 연산.
size : 스택에 있는 데이터의 개수를 반환하는 연산.
4️⃣ 스택(Stack)의 실제 응용 사례.
웹 브라우저의 방문 기록(뒤로 가기 기능)
함수 호출시의 호출 스택
역폴란드 표기법 계산 등
5️⃣ 스택(Stack)의 구현.
스택은 배열이나 연결 리스트를 이용하여 구현할 수 있습니다.
배열을 이용한 스택 구현은 고정된 크기를 가지며, 연결 리스트를 이용한 스택 구현은 동적으로 크기를 조절할 수 있습니다.
배열을 이용한 스택 : 고정된 크기의 배열을 사용하여 스택을 구현할 수 있습니다. 이 경우 스택의 크기가 초과되면 더 큰 배열로 복사하는 추가 작업이 필요할 수 있습니다.
연결 리스트를 이용한 스택 : 동적으로 크기를 조절할 수 있는 연결 리스트를 사용하여 스택을 구현할 수 있습니다. 연결 리스트의 노드 삽입 및 삭제는 O(1)의 시간 복잡도를 가지므로, 스택 연산을 효율적으로 수행할 수 있습니다.
6️⃣ 시간 복잡도
스택의 각 연산은 일반적으로 다음과 같은 시간 복잡도를 가집니다.
Push : O(1)
데이터를 스택의 맨 위에 추가하는 연산은 항상 일정한 시간 내에 완료됩니다.
Pop : O(1)
데이터를 스택의 맨 위에서 제거하는 연산도 항상 일정한 시간 내에 완료됩니다.
Peek 또는 Top : O(1)
스택의 맨 위에 있는 데이터를 확인하는 연산은 데이터 접근만 필요하기 때문에 일정한 시간 내에 완료됩니다.
isEmpty : O(1)
스택이 비어 있는지 확인하는 연산은 스택의 크기만 확인하면 되프로 일정한 시간 내에 완료됩니다.
Size : O(1)
스택에 있는 데이터의 개수를 반환하는 연산은 스택의 크기 정보를 유지하고 있으면 일정한 시간 내에 완료됩니다.
7️⃣ 스택 구현.
// Stack
public class Stack {
private int maxSize; // 스택의 최대 크기
private int top; // 스택의 맨 위를 가리키는 인덱스
private int[] stackArray; // 스택을 저장할 배열
// 생성자
public Stack(int size) {
maxSize = size;
stackArray = new int[maxSize];
top = -1; // 스택이 비어있음을 나타냄
}
// 스택에 값을 푸시하는 메소드
public void push(int value) {
if (isFull()) {
System.out.println("스택이 가득 찼습니다.");
return;
}
stackArray[++top] = value;
}
// 스택에서 값을 팝하는 메소드
public int pop() {
if (isEmpty()) {
System.out.println("스택이 비어있습니다.");
return -1; // 에러를 나타내기 위해 -1 반환
}
return stackArray[top--];
}
// 스택의 맥 위 값을 반환하는 메소드
public int peek() {
if (isEmpty()) {
System.out.println("스택이 비어있습니다.");
return -1; // 에러를 나타내기 위해 -1 반환
}
return stackArray[top];
}
// 스택이 비어있는지 확인하는 메소드
public boolean isEmpty() {
return (top == -1);
}
// 스택이 가득 찼는지 확인하는 메소드
public boolean isFull() {
return (top == maxSize -1);
}
// 스택의 크기를 반환하는 메소드
public int size() {
return top + 1;
}
}
// Main
public class Main {
public static void main(String[] args) {
Stack stack = new Stack(5); // 크기가 5인 스택 생성
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
stack.push(5);
System.out.println("스택의 맨 위 값 : " + stack.peek());
System.out.println("스택의 크기 : " + stack.size());
while (!stack.isEmpty()) {
System.out.println("팝 : " + stack.pop());
}
System.out.println("스택의 크기 : " + stack.size());
}
}
/*
===출력===
스택의 맨 위 값 : 5
스택의 크기 : 5
팝 : 5
팝 : 4
팝 : 3
팝 : 2
팝 : 1
스택의 크기 : 0
*/
주요 메서드 설명
push(int value) : 스택의 맨 위에 값을 추가합니다. 스택이 가득 찼을 경우, 에러 메시지를 출력합니다.
pop() : 스택의 맨 위 값을 제거하고 반환합니다. 스택이 비어 있을 경우, 에러 메시지를 출력하고 -1을 반환합니다.
peek() : 스택의 맨 위 값을 반환하지만, 스택에서 제거하지는 않습니다. 스택이 비어 있을 경우, 에러 메시지를 출력하고 -1을 반환합니다.
isEmpty() : 스택이 비어 있는지 여부를 확인합니다.
isFull() : 스택이 가득 찼는지 여부를 확인합니다.
size() : 스택에 현재 저장된 데이터의 개수를 반환합니다.
-
-
📦[DS,Algorithm] 트리(Tree)
1️⃣ 트리(Tree).
트리(Tree) 는 계층적 구조를 나타내는 자료구조로, 노드(Node)와 에지(Edge)로 구성됩니다.
트리는 사이클이 없는 연결 그래프(Connected Graph)이며, 계층적 데이터 표현에 매우 유용합니다.
트리는 부모-자식 관계를 가지며, 데이터의 조직화와 검색, 계층적 데이터 표현에 사용됩니다.
1️⃣ 트리의 구성 요소.
노드(Node) : 트리의 기본 단위로, 데이터를 저장합니다.
에지(Edge) : 노드와 노드를 연결하는 선으로, 부모-자식 관계를 나타냅니다.
루트(Root) : 트리의 최상위 노드로, 부모 노드가 없습니다.
부모(Parent) : 다른 노드를 가리키는 노드입니다.
자식(Child) : 부모 노드에 의해 가리켜지는 노드입니다.
잎(Leaf) : 자식 노드가 없는 노드입니다.
내부 노드(Internal Node) : 자식 노드가 있는 노드입니다.
레벨(Level) : 루트 노드에서 특정 노드까지의 에지 수를 나타냅니다.
높이(Height) : 트리의 최대 레벨을 의미합니다.
2️⃣ 트리의 특성.
계층적 구조 : 트리는 계층적 구조로 데이터를 조직화합니다.
사이클 없음 : 트리는 사이클이 없는 그래프입니다.
연결성 : 모든 노드는 하나의 연결된 구성 요소로 되어 있습니다.
한 개의 루트 노드 : 트리는 하나의 루트 노드를 가지며, 루트 노드는 트리의 시작점입니다.
3️⃣ 트리의 종류.
이진 트리(Binary Tree) : 각 노드가 최대 두 개의 자식 노드를 가질 수 있는 트리입니다.
이진 탐색 트리(Binary Search Tree, BST) : 이진 트리의 일종으로, 왼쪽 자식 노드의 값이 부모 노드의 값보다 작고, 오른쪽 자식 노드의 값이 부모 노드의 값보다 큰 특성을 가집니다.
균형 이진 트리(Balanced Binary Tree) : AVL 트리, 레드-블랙 트리 등과 같이 트리의 높이가 균형을 이루도록 유지하는 트리입니다.
B-트리(B-Tree) : 데이터베이스와 파일 시스템에서 사용되는 트리로, 자식 노드의 수가 정해진 다진 트리(Multiway Tree)입니다.
힙(Heap) : 완전 이진 트리의 일종으로, 부모 노드의 값이 자식 노드의 값보다 크거나 작은 특성을 가집니다.
트라이(Trie) : 문자열 검색을 위한 트리 자료구조로, 접두사 검색에 유용합니다.
4️⃣ 트리의 주요 연산.
삽입(Insertion) : 트리에 새로운 노드를 추가합니다.
삭제(Deletion) : 트리에서 노드를 제거합니다.
탐색(Search) : 트리에서 특정 값을 찾습니다.
순회(Traversal) : 트리의 모든 노드를 방문합니다. 전위(Preorder), 중위(Inorder), 후위(Postorder), 레벨 순회(Level Order) 방식이 있습니다.
-
-
📦[DS,Algorithm] 완전 이진 트리(Complete Binary Tree)
1️⃣ 완전 이진 트리(Complete Binary Tree).
완전 이진 트리(Complete Binary Tree)는 이진 트리의 한 종류입니다.
1️⃣ 완전 이진 트리(Complete Binary Tree)의 특성.
모든 레벨이 완전히 채워져 있다.
마지막 레벨을 제외한 모든 레벨의 노드가 최대 개수로 채워져 있습니다.
마지막 레벨의 노드들은 왼쪽부터 오른쪽으로 채워져 있습니다.
노드의 배치
트리의 높이가 ‘h’ 라면, 마지막 레벨을 제외한 모든 레벨에는 ‘2^k’ 개의 노드가 있습니다. 여기서 ‘k’ 는 해당 레벨의 깊이 입니다.
마지막 레벨에는 1개 이상 ‘2^h’ 개 이하의 노드가 있으며, 이 노드들은 왼쪽부터 채워집니다.
2️⃣ 완전 이친 트리의 예.
1
/ \
2 3
/ \ / \
4 5 6 7
/ \
8 9
위의 트리는 완전 이진 트리의 예입니다.
모든 레벨이 완전히 채워져 있고, 마지막 레벨의 노드들은 왼쪽부터 오른쪽으로 채워져있습니다.
3️⃣ 완전 이진 트리의 속성.
노드 수
높이가 ‘h’ 인 완전 이진 트리는 최대 ‘2^(h+1) - 1’ 개의 노드를 가질 수 있습니다.
마지막 레벨을 제외한 모든 노드는 ‘2^h - 1’ 개의 노드를 가집니다.
높이
노드 수가 ‘n’ 인 완전 이진 트리의 높이는 ‘O(log n)’ 입니다.
배열 표현
완전 이진 트리는 배열을 사용하여 쉽게 표현할 수 있습니다. 이는 힙 자료구조에서 많이 사용됩니다.
4️⃣ 배열을 통한 완전 이진 트리 표현
완전 이진 트리는 배열을 사용하여 효율적으로 표현할 수 있습니다.
노드의 인덱스를 기준으로 부모-자식 관계를 쉽게 파악할 수 있습니다.
노드의 인덱스 규칙
루트 노드 : 인덱스 0
인덱스 ‘i’의 오른쪽 자식 노드 : ‘2*i + 1’
인덱스 ‘i’의 부모 노드 : ‘(i - 1) / 2’
5️⃣ 예제 코드
아래는 완전 이진 트리를 배열로 표현하고, 이를 출력하는 간단한 예제 코드입니다.
public class CompleteBinaryTree {
public static void main(String[] args) {
int[] tree = {1, 2, 3, 4, 5, 6, 7, 8, 9};
// 트리 출력
printTree(tree);
}
// 배열로 표현된 완전 이진 트리 출력
public static void printTree(int[] tree) {
for (int i = 0; i < tree.length; i++) {
int leftChildIndex = 2 * i + 1;
int rightChildIndex = 2 * i + 2;
System.out.print("Node " + tree[i] + ": ");
if (leftChildIndex < tree.length) {
System.out.print("Left Child: " + tree[leftChildIndex] + ", ");
} else {
System.out.print("Left Child: null, ");
}
if (rightChildIndex < tree.length) {
System.out.print("Right Child: " + tree[rightChildIndex]);
} else {
System.out.print("Right Child: null");
}
System.out.println();
}
}
}
/* 출력
Node 1: Left Child: 2, Right Child: 3
Node 2: Left Child: 4, Right Child: 5
Node 3: Left Child: 6, Right Child: 7
Node 4: Left Child: 8, Right Child: 9
Node 5: Left Child: null, Right Child: null
Node 6: Left Child: null, Right Child: null
Node 7: Left Child: null, Right Child: null
Node 8: Left Child: null, Right Child: null
Node 9: Left Child: null, Right Child: null
*/
설명
트리 배열 초기화 : int[] tree = {1, 2, 3, 4, 5, 6, 7, 8, 9};
완전 이진 트리를 배열로 표현합니다.
트리 출력 : printTree(tree)
배열로 표현된 완전 이진 트리를 출력하는 함수입니다.
각 노드에 대해 왼쪽 자식과 오른쪽 자식을 출력합니다.
시간 복잡도
삽입(Insertion) : O(log n)
삭제(Deletion) : O(log n)
탐색(Search) : O(n) (일반적으로 완전 이진 트리는 탐색보다 삽입/삭제가 주된 연산입니다.)
완전 이진 트리는 데이터의 구조적 특성 때문에 힙과 같은 자료구조에서 많이 사용됩니다.
이는 효율적인 삽입 및 삭제 연산을 제공하며, 배열을 통한 표현이 간편하여 다양한 알고리즘에서 유용하게 사용됩니다.
-
📦[DS,Algorithm] 이진 트리(Binary Tree)
1️⃣ 이진 트리(Binary Tree).
이진 트리(Binary Tree) 는 각 노드가 최대 두 개의 자식 노드를 가질 수 있는 트리 구조입니다.
이 두 자식 노드는 일반적으로 왼쪽 자식(Left Child) 과 오른쪽 자식(Right Child) 이라고 불립니다.
이진 트리는 다양한 응용 프로그램에서 중요한 자료구조입니다.
1️⃣ 이진 트리의 구성 요소.
노드(Node) : 데이터를 저장하는 기본 단위입니다.
루트(Root) : 트리의 최상위 노드입니다.
자식(Child) : 특정 노드로부터 연결된 하위 노드입니다.
부모(Parent) : 특정 노드를 가리키는 상위 노드입니다.
잎(Leaf) : 자식 노드가 없는 노드입니다.
서브트리(Subtree) : 특정 노드와 그 노드의 모든 자식 노드로 구성된 트리입니다.
2️⃣ 이진 트리의 종류.
포화 이진 트리(Full Binary Tree) : 모든 노드가 0개 또는 2개의 자식 노드를 가지는 트리입니다.
완전 이진 트리(Complete Binary Tree) : 마지막 레벨을 제외한 모든 레벨이 완전히 채워져 있으며, 마지막 레벨의 노드는 왼쪽부터 채워져 있는 트리입니다.
높이 균형 이진 트리(Height-balanced binary Tree) : AVL 트리와 같이 각 노드의 왼쪽 서브트리와 오른쪽 서브트리의 높이 차이가 1 이하인 트리입니다.
이진 탐색 트리(Binary Search Tree, BST) : 왼쪽 서브트리의 모든 노드가 루트 노드보다 작고, 오른쪽 서브 트리의 모든 노드가 루트 노드보다 큰 트리입니다.
3️⃣ 이진 트리의 주요 연산 및 시간 복잡도.
삽입(Insertion) : 새로운 노드를 트리에 추가합니다.
일반적인 경우 시간 복잡도 : O(log n)(이진 탐색 트리에서)
최악의 경우 시간 복잡도 : O(n)(편향된 트리에서)
삭제(Deletion) : 트리에서 특정 노드를 제거합니다.
일반적인 경우 시간 복잡도 : O(log n)(이진 탐색 트리에서)
최악의 경우 시간 복잡도: O(n)(편향된 트리에서)
탐색(Search) : 트리에서 특정 값을 찾습니다.
일반적인 경우 시간 복잡도: O(log n)(이진 탐색 트리에서)
최악의 경우 시간 복잡도 : O(n)(편향된 트리에서)
순회(Traversal) : 트리의 모든 노드를 방문합니다. 순회 방법에는 전위(Preorder), 중위(Inorder), 후위(Postorder) 순회가 있습니다.
시간 복잡도: O(n)(모든 노드를 방문하기 때문에)
4️⃣ 이진 트리의 예제
이진 탐색 트리(BST)의 구현
// TreeNode
public class TreeNode {
int data;
TreeNode left;
TreeNode right;
public TreeNode(int data) {
this.data = data;
this.left = null;
this.right = null;
}
}
// BinarySearchTree
public class BinarySearchTree {
private TreeNode root;
public BinarySearchTree() {
this.root = null;
}
// 삽입 연산
public void insert(int data) {
root = insertRec(root, data);
}
private TreeNode insertRec(TreeNode root, int data) {
if (root == null) {
root = new TreeNode(data);
return root;
}
if (data < root.data) {
root.left = insertRec(root.left, data);
} else if (data > root.data) {
root.right = insertRec(root.right, data);
}
return root;
}
// 탐색 연산
public boolean search(int data) {
return searchRec(root, data);
}
private boolean searchRec(TreeNode root, int data) {
if (root == null) {
return false;
}
if (root.data == data) {
return true;
}
if (data < root.data) {
return searchRec(root.left, data);
} else {
return searchRec(root.right, data);
}
}
// 중위 순회(Inorder Traversal)
public void inorder() {
inorderRec(root);
}
private void inorderRec(TreeNode root) {
if (root != null) {
inorderRec(root.left);
System.out.print(root.data + " ");
inorderRec(root.right);
}
}
}
// Main
public class Main {
public static void main(String[] args) {
BinarySearchTree bst = new BinarySearchTree();
bst.insert(50);
bst.insert(30);
bst.insert(20);
bst.insert(40);
bst.insert(70);
bst.insert(60);
bst.insert(80);
System.out.println("Inorder traversal of the BST:");
bst.inorder(); // 출력: 20 30 40 50 60 70 80
System.out.println("\nSearch for 40: " + bst.search(40)); // 출력: true
System.out.println("Search for 90: " + bst.search(90)); // 출력: false
}
}
-
-
📦[DS,Algorithm] 해시 테이블(Hash Table)
1️⃣ 해시 테이블(Hash Table).
해시 테이블(Hash Table)은 데이터를 키-값 쌍(key-value pairs)으로 저장하는 자료구조입니다.
해시 테이블은 해시 함수를 사용하여 키를 해시 값으로 변환하고, 이 해시 값을 인덱스로 사용하여 배열에서 값을 저장하거나 검색합니다.
이를 통해 데이터에 빠르게 접근할 수 있습니다.
1️⃣ 해시 테이블의 구성 요소.
키(key) : 각 데이터를 식별하기 위한 고유한 식별자입니다.
값(Value) : 키와 연관된 데이터입니다.
해시 함수(Hash Function) : 키를 입력으로 받아 해시 값을 출력하는 함수입니다. 이 해시 값은 보통 정수이며, 배열의 인덱스로 사용됩니다.
버킷(Bucket) : 해시 값에 의해 인덱싱되는 배열의 각 위치입니다. 각 버킷은 하나의 키-값 쌍 또는 충돌 처리를 위한 데이터 구조(예: 연결 리스트)를 저장할 수 있습니다.
2️⃣ 해시 함수의 역할.
해시 함수는 키를 고정된 크기의 해시 값으로 매핑합니다.
이상적인 해시 함수는 키를 균등하게 분포시키고, 충돌을 최소화합니다.
3️⃣ 충동(Collision)과 충돌 해결 방법.
두 개 이상의 키가 동일한 해시 값을 가질 때 충돌이 발생합니다.
해시 테이블은 이러한 충돌을 처리하기 위한 여러가지 방법을 제공합니다.
체이닝(Chaining) : 각 버킷에 연결 리스트를 사용하여 동일한 해시 값을 갖는 모든 요소를 저장합니다. 충돌이 발생하면 해당 버킷의 리스트에 요소를 추가합니다.
개방 주소법(Open Addressing) : 충돌이 발생하면 다른 빈 버킷을 찾아 데이터를 저장합니다. 이를 위해 다양한 탐사 방법(예: 선형 탐사, 제곱 탐사, 이중 해싱)을 사용합니다.
4️⃣ 해시 테이블의 시간 복잡도.
검색(Search) : O(1)(평균), O(n)(최악)
삽입(Insertion) : O(1)(평균), O(n)(최악)
삭제(Deletion) : O(1)(평균), O(n)(최악)
최악의 경우 시간 복잡도는 해시 충돌로 인해 모든 요소가 하나의 버킷에 저장될 때 발생합니다.
그러나, 좋은 해시 함수와 충돌 해결 방법을 사용하면 평균적으로 O(1)의 성능을 유지할 수 있습니다.
5️⃣ 해시 테이블의 장점과 단점.
장점
빠른 검색, 삽입, 삭제 성능(평균적으로 O(1))
키를 사용하여 데이터에 빠르게 접근 가능
단점
해시 함수의 성능에 의존
충돌 처이 필요
메모리 사용량이 증가할 수 있슴(특히 체이닝을 사용하는 경우)
💻 해시 테이블의 구현 예제.
아래는 Java에서 간단한 해시 테이블을 구현한 예제입니다.
// HashTable
import java.util.LinkedList;
class HashTable {
private class HashNode {
String key;
String value;
HashNode next;
public HashNode(String key, String value) {
this.key = key;
this.value = value;
}
}
private LinkedList<HashNode>[] buckets;
private int numBuckets;
private int size;
public HashTable() {
numBuckets = 10; // 버킷의 초기 크기
buckets = new LinkedList[numBuckets];
size = 0;
for (int i = 0; i < numBuckets; i++) {
buckets[i] = new LinkedList<>();
}
}
private int getBucketIndex(String key) {
int hashCode = key.hashCode();
int index = hashCode % numBuckets;
return index < 0 ? index * -1 : index;
}
public void put(String key, String value) {
int bucketIndex = getBucketIndex(key);
LinkedList<HashNode> bucket = buckets[bucketIndex];
for (HashNode node : bucket) {
if (node.key.equals(key)) {
node.value = value;
return;
}
}
bucket.add(new HashNode(key, value));
size++;
}
public String get(String key) {
int bucketIndex = getBucketIndex(key);
LinkedList<HashNode> bucket = buckets[bucketIndex];
for (HashNode node : bucket) {
if (node.key.equals(key)) {
return node.value;
}
}
return null;
}
public String remove(String key) {
int bucketIndex = getBucketIndex(key);
LinkedList<HashNode> bucket = buckets[bucketIndex];
HashNode prev = null;
for (HashNode node : bucket) {
if (node.key.equals(key)) {
if (prev != null) {
prev.next = node.next;
} else {
bucket.remove(node);
}
size--;
return node.value;
}
prev = node;
}
return null;
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
}
// Main
public class Main {
public static void main(String[] args) {
HashTable hashTable = new HashTable();
hashTable.put("one", "1");
hashTable.put("two", "2");
hashTable.put("three", "3");
System.out.println("Value for key 'one': " + hashTable.get("one"));
System.out.println("Value for key 'two': " + hashTable.get("two"));
System.out.println("Removing key 'one': " + hashTable.remove("one"));
System.out.println("Contains key 'one': " + (hashTable.get("one") != null));
}
}
/*
출력
Value for key 'one': 1
Value for key 'two': 2
Removing key 'one': 1
Contains key 'one': false
*/
-
-
📦[DS,Algorithm] 해시(Hash)
1️⃣ 해시(Hash).
해시(Hash)란 컴퓨터 과학에서 주어진 입력 데이터를 고정된 크기의 고유한 값(일반적으로 숫자)으로 변환하는 과정 또는 그 결과 값을 말합니다.
해시는 주로 데이터 검색, 데이터 무결성 검증, 암호화 등에 사용됩니다.
1️⃣ 해시의 개념.
해시 함수(Hash Function)
임의의 길이를 가진 데이터를 고정된 길이의 해시 값으로 변환하는 함수입니다.
해시 함수는 동일한 입력에 대해 항상 동일한 해시 값을 생성해야 하며, 서로 다른 입력에 대해서는 가능한 한 다른 해시 값을 생성해야 합니다.
해시 값(Hash Value)
해시 함수를 통해 생성된 고정된 크기의 출력 값입니다.
이를 해시 코드(Hash Code) 또는 다이제스트(Digest)라고도 합니다.
2️⃣ 해시 함수의 특징.
결정성(Deterministic) : 동일한 입력에 대해 항상 동일한 해시 값을 반환합니다.
효율성(Efficiency) : 해시 함수는 입력 데이터를 빠르게 처리하여 해시 값을 생성해야 합니다.
충돌 저항성(Collision Resistance) : 서로 다른 두 입력이 동일한 해시 값을 갖지 않도록 해야 합니다. 현실적으로 완벽한 충돌 저항성은 불가능하므로, 가능한 충돌을 최소화하는 것이 중요합니다.
역상 저항성(Pre-image Resistance) : 해시 값을 통해 원해의 입력 데이터를 유추하는 것이 어렵거나 불가능해야 합니다.
두 번째 역상 저항성(Second Pre-image Resitance) : 특정 입력과 동일한 해시 값을 갖는 또 다른 입력을 찾는 또 다른 입력을 찾는 것이 어려워야 합니다.
3️⃣ 해시 함수의 용도.
데이터 검색 : 해시 테이블(Hash Table)과 같은 자료구조에서 빠른 데이터 검색을 위해 사용됩니다.
데이터 무결성 검증 : 데이터가 변경되지 않았음을 확인하기 위해 해시 값을 사용합니다. 예를 들어, 파일의 해시 값을 비교하여 파일이 손상되지 않았음을 확인할 수 있습니다.
암호화 및 보안 : 패스워드 저장, 디지털 서명, 메시지 인증 코드(MAC) 등에서 데이터의 무결성과 기밀성을 보장하기 위해서 사용됩니다.
4️⃣ 해시 함수의 예
SHA-256(Secure Hash Algorithm 256-bit) : 256비트의 해시 값을 생성하는 암호화 해시 함수입니다.
MD5(Message Digest Algorithm 5) : 128비트의 해시 값을 생성하는 해시 함수로, 현재는 충돌 저항성의 취약성 때문에 보안 용도로는 권장되지 않습니다.
CRC32(Cyclic Redundancy Check 32-bit) : 데이터 전송 오류 검출을 위해 사용되는 32비트 해시 함수입니다.
🙋♂️ 주요 포인트 요약
해시(Hash) 는 데이터를 고정된 크기의 고유한 값으로 변환하는 과정입니다.
해시 함수는 빠르고 효율적으로 해시 값을 생성하며, 충돌을 최소화하고 역상을 예측할 수 없도록 설계되어야 합니다.
해시 함수는 데이터 검색, 무결성 검증, 암호화 등 다양한 용도로 사용됩니다.
💻 해시 함수의 예제 코드
아래는 Java에서 SHA-256 해시 함수를 사용하여 문자열의 해시 값을 생성하는 예제입니다.
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Main {
public static void main(String[] args) {
String input = "Hello World!";
try {
// SHA-256 해시 함수 인스턴스 생성
MessageDigest digest = MessageDigest.getInstance("SHA-256");
// 입력 문자열의 해시 값 계산
byte[] hash = digest.digest(input.getBytes());
// 해시 값을 16진수 문자열로 변환하여 출력
System.out.println("Hash value: " + bytesToHex(hash));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
// 바이트 배열을 16진수 문자열로 변환하는 함수
private static String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
}
}
-
-
-
-
-
-
📦[DS,Algorithm] 선형 자료구조 - 배열
1️⃣ 선형 자료구조 - 배열.
자료구조 관점에서 배열을 이해하고 여러 방법으로 구현 가능
1️⃣ 배열(Array).
자료구조 관점에서 배열(Array)은 동일한 타입의 데이터를 연속된 메모리 공간에 저장하는 선형 자료구조입니다.
배열은 조정된 크기를 가지며, 인덱스를 사용하여 각 요소에 빠르게 접근할 수 있는 특징이 있습니다.
배열은 가장 기본적이고 널리 사용되는 자료구조 중 하나입니다.
특징.
고정된 크기(Fixed Size)
배열은 선언 시 크기가 결정되며, 배열의 크기는 변경할 수 없습니다. 이 크기는 배열을 사용하는 동안 고정되어 있습니다.
예: ‘int[] numbers = new int[10];‘(크기가 10인 정수형 배열)
연속된 메모리 공간(Contiguous Memory Allocation)
배열의 요소들은 메모리상에 연속적으로 배치됩니다. 이는 인덱스를 통한 빠른 접근을 가능하게 합니다.
첫 번째 요소의 메모리 주소를 기준으로 인덱스를 사용하여 다른 요소의 주소를 계산할 수 있습니다.
인덱스를 통한 접근(Indexing)
배열의 각 요소는 인덱스를 통해 접근할 수 있습니다. 인덱스는 0부터 시작하여 배열의 크기 -1까지의 값을 가집니다.
예: ‘numbers[0]’,’numbers[1]‘,…,’numbers[9]‘
동일한 데이터 타입(Homogeneous Data Type)
배열은 동일한 데이터 타입의 요소들로 구성됩니다. 즉, 배열 내 모든 요소는 같은 데이터 타입이어야 합니다.
예: 정수형 배열, 문자열 배열 등.
장점.
빠른 접근 속도(Fast Access) :
인덱스를 사용하여 O(1) 시간 복잡도로 배열의 임의의 요소에 접근할 수 있습니다. 이는 배열의 주요 장점 중 하나입니다.
간단한 구현(Simple Implementation) :
배열은 데이터 구조가 간단하여 구현이 용이합니다. 기본적인 자료구조로, 다른 복잡한 자료구조의 기초가 됩니다.
단점.
고정된 크기(Fixed Size) :
배열의 크기는 선언 시 결정되며, 크기를 변경할 수 없습니다. 이는 크기를 사전에 정확히 예측하기 어려운 경우 비효율적일 수 있습니다.
삽입 및 삭제의 비효율성(Inefficient Insertions and Deletions) :
배열의 중간에 요소를 삽입하거나 삭제할 경우, 요소들을 이동시켜야 하기 때문에 O(n) 시간이 소요됩니다. 이는 큰 배열의 경우 성능 저하를 초래할 수 있습니다.
메모리 낭비(Memory Waste) :
배열의 크기를 너무 크게 설정하면 사용되지 않는 메모리가 낭비될 수 있고, 너무 작게 설정하면 충분한 데이터를 저장할 수 없습니다.
배열의 사용 예시.
정수형 배열 선언 및 초기화
int[] numbers = new int[5];
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
numbers[3] = 40;
numbers[4] = 50;
배열의 요소 접근
int firstElement = numbers[0]; // 10
int lastElement = numbers[4]; // 50
배열의 순회
for (int i = 0; i < numbers.length; i++) {
System.out.println(numbers[i]);
}
마무리.
배열은 다양한 상황에서 기본적인 데이터 저장과 접근 방법을 제공하며, 특정 요구사항에 맞춰 다른 자료구조와 함께 사용되기도 합니다.
배열의 빠른 접근 속도와 간단한 구조 덕분에, 많은 알고리즘과 프로그램에서 핵심적인 역할을 합니다.
2️⃣ 배열 직접 구현.
// CustomArray 클래스
public class CustomArray {
private int[] data;
private int size;
// 특정 용량으로 배열을 초기화하는 생성자
public CustomArray(int capacity) {
data = new int[capacity];
size = 0;
}
// 배열의 크기를 가져오는 메서드
public int size() {
return size;
}
// 배열이 비어 있는지 확인하는 메서드
public boolean isEmpty() {
return size == 0;
}
// 특정 인덱스의 요소를 가져오는 메서드
public int get(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Index out of bounds");
}
return data[index];
}
// 특정 인덱스에 요소를 설정하는 메서드
public void set(int index, int value) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Index out of bounds");
}
data[index] = value;
}
// 배열에 요소를 추가하는 메서드
public void add(int value) {
if (size == data.length) {
throw new IllegalStateException("Array is full");
}
data[size] = value;
size++;
}
// 특정 인덱스의 요소를 삭제하는 메서드
public void remove(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Index out of bounds");
}
for (int i = index; i < size - 1; i++) {
data[i] = data[i + 1];
}
size--;
}
// 모든 요소를 출력하는 메서드
public void print() {
for (int i = 0; i < size; i++) {
System.out.print(data[i] + " ");
}
System.out.println();
}
}
설명.
필드:
‘data’ : 실제 데이터를 저장하는 배열.
‘size’ : 현재 배열에 저장된 요소의 개수.
생성자:
‘CustomArray(int capacity)’ : 초기 용량을 설정하여 배열을 초기화 합니다.
메서드:
‘size()’ : 현재 배열에 저장된 요소의 개수를 반환합니다.
‘isEmpty()’ : 배열이 비어있는지 확인합니다.
‘get(int index)’ : 특정 인덱스의 요소를 반환합니다.
‘set(int index, int value)’ : 특정 인덱스의 요소를 설정합니다.
‘add(int value)’ : 배열의 마지막에 요소를 추가합니다.
‘remove(int index)’ : 특정 인덱스의 요소를 제거하고, 이후의 요소들을 앞으로 이동시킵니다.
‘print()’ : 배열의 모든 요소를 출력합니다.
-
📦[DS,Algorithm] 자료구조 소개
1️⃣ 자료구조(Data Structure)
자료구조(Data Structure)는 데이터를 효율적으로 저장하고 관리하며, 이를 통해 데이터를 효율적으로 접근하고 수정할 수 있도록 하는 체계적인 방법입니다.
자료구조는 알고리즘의 성능을 최적화하고 프로그램의 효율성을 향상시키는 데 중요한 역할을 합니다.
기본 개념.
자료구조는 데이터를 저장하는 방식과 데이터를 조작하는 방법을 정의합니다.
이는 데이터를 어떻게 배열하고, 접근하고, 수정하고, 삭제할지를 규정하는 규칙과 방법의 집합입니다.
주요 목적.
효율적인 데이터 저장 및 접근.
데이터를 효율적으로 저장하여 빠르게 접근하고 검색할 수 있도록 합니다.
데이터 수정 및 삭제 용이.
데이터를 쉽게 수정하고 삭제할 수 있도록 합니다.
알고리즘 최적화.
알고리즘의 성능을 최적화하고 실행 시간을 단축시킵니다.
주요 종류.
배열(Array)
고정된 크기의 연속된 메모리 공간에 데이터를 저장합니다.
인덱스를 사용하여 데이터에 빠르게 접근할 수 있습니다.
연결 리스트(Linked List)
각 요소가 데이터와 다음 요소를 가리키는 포인터를 포함합니다.
동적 크기 조절이 가능하며 삽입과 삭제가 용이합니다.
스택(Stack)
후입선출(LIFO, Last In First Out) 방식으로 동작합니다.
데이터를 삽입하는 push와 삭제하는 pop 연산을 가집니다.
큐(Queue)
선입선출(FIFO, First In First Out) 방식으로 동작합니다.
데이터를 삽입하는 enqueue와 삭제하는 dequeue 연산을 가집니다.
트리(Tree)
계층적 구조를 가지며, 노드와 에지로 구성됩니다.
이진 트리, 이진 탐색 트리, AVL 트리 등 다양한 형태가 있습니다.
그래프(Graph)
노드(정점)와 에지(간선)로 구성된 자료구조로, 다양한 관계를 표현할 수 있습니다.
방향 그래프, 무방향 그래프 등이 있습니다.
해시 테이블(Hash Table)
키-값 쌍을 저장하며, 해시 함수를 사용하여 데이터에 빠르게 접근할 수 있습니다.
충돌 해결 방법으로 체이닝과 개방 주소법이 있습니다.
응용 사례.
자료구조는 데이터베이스, 운영체제, 네트워크, 인공지능, 게임 개발 등 다양한 분야에서 중요한 역할을 합니다.
적절한 자료구조의 선택은 프로그램의 성능과 효율성을 크게 향상시킬 수 있습니다.
2️⃣ 자료구조의 분류
1️⃣ 선형 자료구조(Linear Data Structure)
선형 자료구조(Linear Data Structure)는 데이터 요소들이 순차적으로 배열된 구조를 의미합니다.
이 자료구조에서는 데이터 요소들이 직선 형태로 연결되어 있으며, 각 요소는 한 다음 요소나 이전 요소와 연결되어 있습니다.
선형 자료구조의 주요 특징인 데이터 요소들이 한 줄로 배열되어 있다는 점 입니다.
주요 선형 자료구조르는 배열, 연결 리스트, 스택, 큐 등이 있습니다.
주요 선형 자료구조.
배열(Array)
정의 : 동일한 타입의 데이터 요소들이 연속된 메모리 공간에 저장되는 자료구조입니다.
특징 : 고정된 크기를 가지며 인덱스를 통해 데이터에 빠르게 접근할 수 있습니다.
예시 : 정수형 배열, 문자열 배열 등.
연결 리스트(Linked List)
정의 : 각 데이터 요소가 노드로 구성되고, 각 노드는 데이터와 다음 노드를 가리키는 포인터를 포함하는 자료구조입니다.
특징 : 동적 크기 조절이 가능하며 삽입과 삭제가 용이하지만, 인덱스를 통한 접근은 배열보다 느립니다.
종류 : 단일 연결 리스트, 이중 연결 리스트, 원형 연결 리스트 등.
스택(Stack)
정의 : 후입선출(LIFO, Last In First Out) 방식으로 동작하는 자료구조입니다.
특징 : 데이터 삽입(push)과 삭제(pop)이 한쪽 끝에서만 이루어집니다.
사용 사례 : 함수 호출 스택, 역순 문자열 처리 등.
큐(Queue)
정의 : 선입선출(FIFO, First In First Out) 방식으로 동작하는 자료구조입니다.
특징 : 데이터의 삽입(enqueue)은 한쪽 끝(후단)에서, 삭제(dequeue)는 반대쪽 끝(전단)에서 이루어집니다.
종류 : 원형 큐, 우선순위 큐, 덱(Deque) 등.
사용 사례 : 운영 체제의 작업 스케줄링, 프린터 대기열 등.
선형 자료구조의 특징 및 장단점.
특징.
순차적 접근이 가능하며, 데이터를 차례대로 처리할 때 유리합니다.
메모리에서 연속적으로 배치되므로 인덱스를 통해 직접 접근할 수 있습니다.(배열의 경우)
장점.
간단하고 구현이 용이합니다.
데이터의 삽입과 삭제가 특정 조건 하에 효율적일 수 있습니다(예: 스택, 큐)
단점
데이터 크기에 따라 메모리 낭비가 발생할 수 있습니다(배열의 경우).
특정 요소 접근이나 삽입/삭제 시 성능이 저하될 수 있습니다(연결 리스트의 경우)
마무리.
선형 자료구조는 데이터가 순차적으로 연결되어 있어 순차적 처리에 적합하며, 프로그램의 다양한 부분에서 사용되는 기초적인 자료구조입니다.
2️⃣ 비선형 자료구조(Non-linear Data Structure)
비선형 자료구조(Non-linear Data Structure)는 데이터 요소들이 계층적 또는 그물 형태로 배열된 구조를 의미합니다.
이 자료구조에서는 데이터 요소들이 순차적으로 배열되지 않고, 하나의 요소가 여러 요소들과 연결될 수 있습니다.
주요 비선형 자료구조로는 트리(Tree)와 그래프(Graph)가 있습니다.
주요 비선형 자료구조.
트리(Tree)
정의 : 노드와 그 노드들을 연결하는 간선으로 구성된 계층적 자료구조입니다. 트리는 루트 노드에서 시작하여 자식 노드로 분기하며, 사이클이 없습니다.
특징 : 트리는 계층적 관계를 나타내며, 각 노드는 0개 이상의 자식 노드를 가질 수 있습니다.
종류 :
이진 트리(Binary Tree) : 각 노드가 최대 두 개의 자식 노드를 가지는 트리입니다.
이진 탐색 트리(Binary Search Tree) : 왼쪽 자식은 부모보다 작고, 오른쪽 자식은 부모보다 큰 값을 가지는 이진 트리입니다.
균형 이진 트리(Balanced Binary Tree) : AVL 트리, 레드-블랙 트리 등과 같이 높이가 균형을 이루도록 유지되는 트리입니다.
힙(Heap) : 완전 이진 트리의 일종으로, 최대 힙과 최소 힙이 있습니다.
트라이(Trie) : 문자열을 저장하고 빠르게 검색하기 위해 사용되는 트리입니다.
그래프(Graph)
정의 : 정점(Vertex)들과 이 정점들을 연결하는 간선(Edge)들로 구성된 자료구조입니다.
특징 : 그래프는 방향 그래프(Directed Graph)와 무방향 그래프(Undirceted Graph)로 나눌 수 있으며, 사이클이 존재할 수 있습니다.
종류 :
방향 그래프(Directed Graph) : 간선에 방향성이 있는 그래프입니다.
무방향 그래프(Undirected Graph) : 간선에 방향성이 없는 그래프입니다.
가중치 그래프(Weighted Graph) : 간선에 가중치가 부여된 그래프입니다.
비가중치 그래프(Unweighted Graph) : 간선에 가중치가 없는 그래프입니다.
비선형 자료구조의 특징 및 장단점.
특징 :
계층적 또는 네트워크 구조를 나태내는 데 적합합니다.
복잡한 관계를 표현할 수 있으며, 데이터 요소 간의 다대다 관계를 처리할 수 있습니다.
장점 :
데이터의 계층적 구조를 쉽게 표현할 수 있습니다(트리).
복잡한 연결 관계를 효과적으로 모델링할 수 있습니다(그래프).
특정 유형의 탐색, 정렬, 데이터 압축, 네트워크 라우팅 등에 유용합니다.
단점 :
구현과 관리가 선형 자료구조보다 복잡할 수 있습니다.
특정 작업(예: 트리의 균형 유지, 그래프 탐색 등)에서 추가적인 알고리즘이 필요합니다.
마무리.
비선형 자료구조는 데이터가 단순히 순차적으로 배열되지 않고, 복잡한 관계를 나타내는 경우에 사용됩니다.
예를 들어, 파일 시스템의 디렉터리 구조, 데이터베이스 인덱스, 소셜 네트워크의 사용자 관계 등이 비선형 자료구조를 활용하는 사례입니다.
2️⃣ 자료구조의 구현.
1️⃣ 추상 자료형(Abstract Data Type, ADT)
자바 프로그래밍에서의 추상 자료형(Abstract Data Type, ADT)은 데이터의 논리적 구조와 이를 조작하는 연산들을 명확하게 정의한 개념입니다.
ADT는 구현 세부 사항을 숨기고, 데이터와 연산의 인터페이스를 통해 사용자에게 추상적인 수준에서 데이터 조작을 제공합니다.
즉, ADT는 데이터가 어떻게 저장되고 구현되는지에 대한 정보는 감추고, 데이터와 상호작용하는 방법만을 정의합니다.
주요 개념
추상화(Abstraction)
ADT는 데이터를 추상화하여 데이터의 실제 구현과 독립적으로 사용될 수 있도록 합니다.
사용자는 데이터의 저장 방식이나 연산의 구현 방법을 알 필요 없이, ADT가 제공하는 인터페이스를 통해 데이터를 조작할 수 있습니다.
인터페이스(Interface)
ADT는 데이터 타입과 이를 다루는 연산들을 인터페이스를 통해 정의합니다.
자바에서는 인터페이스(Interface) 키워드를 사용하여 ADT의 연산을 정의할 수 있습니다.
캡슐화(Encapsulation)
ADT는 데이터와 연산을 하나의 단위로 묶어 캡슐화합니다.
데이터를 직접 접근하지 않고, 정의된 연산을 통해서만 접근할 수 있도록 하여 데이터 무결성을 보장합니다.
자바에서의 ADT 예시
다음은 자바에서 스택(Stack) ADT를 정의하고 구현하는 예시입니다.
스택 인터페이스 정의
public interface Stack<T> {
void push(T item); // 스택에 아이템을 추가
T pop(); // 스택에서 아이템을 제거하고 반환
T peek(); // 스택의 맨 위 아이템을 반환(제거하지 않음)
boolean isEmpty(); // 스택이 비어 있는지 확인
int size(); // 스택의 크기 반환
}
스택 구현
import java.util.ArrayList;
import java.util.List;
public class ArrayListStack<T> implements Stack<T> {
private List<T> list = new ArrayList<>();
@Override
public void push(T item) {
list.add(item);
}
@Override
public T pop() {
if (isEmpty()) {
throw new RuntimException("Stack is empty");
}
return list.remove(list.size() - 1);
}
@Override
public T peek() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return list.get(list.size() - 1);
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
@Override
public int size() {
return list.size();
}
}
설명
‘Stack<T>‘ 인터페이스는 스택 ADT의 연산을 정의합니다. 이 인터페이스는 ‘push’, ‘pop’, ‘peek’, ‘isEmpty’, ‘size’ 메서드를 포함합니다.
‘ArrayListStack<T>‘ 클래스는 ‘Stack<T>‘ 인터페이스를 구현합니다. 이 클래스는 ‘ArrayList’ 를 내부 데이터 구조로 사용하여 스택 연산을 구현합니다.
‘push’ 메서드는 스택에 아이템을 추가합니다.
‘pop’ 메서드는 스택에서 맨 위의 아이템을 제거하고 반환합니다.
‘peek’ 메서드는 스택의 맨 위 아이템을 제거하지 않고 반환합니다.
‘isEmpty’ 메서드는 스택이 비어 있는지 확인합니다.
‘size’ 메서드는 스택의 크기를 반환합니다.
이 예시에서 ‘Stack<T>‘ 인터페이스는 스택 ADT를 정의하고, ‘ArrayListStack<T>‘ 클래스는 이 ADT를 구현한 것입니다.
사용자는 ‘ArrayListStack’ 의 내부 구현을 알 필요 없이 ‘Stack’ 인터페이스를 통해 스택 연산을 사용할 수 있습니다.
이는 ADT의 주요 장점 중 하나인 구현의 독립성을 잘 보여줍니다.
-
-
-
-
[Math] 명제와 증명 - 논리적 사고의 기초: 필요조건과 충분 조건.
1️⃣ 기하학 - 설득술로서 발전해 온 수학
1️⃣ 명제와 증명.
“필요조건” 과 “충분조건” 에 대한 이해는 모든 논리의 기초가 되는 가장 중요한 사항이라 해도 과언이 아닙니다.
수학은 논리와 떼려야 뗄 수 없는 관계라는 것은 모두가 아는 사실이지만 ‘필요’ 와 ‘충분’ 은 수학의 논리 중에서도 가장 중요한 역할을 하는 기본적인 사고방식입니다.
‘이것 없이는 어떠한 수학적 논리도 전개할 수 없다’라고 단언할 수 있을 정도입니다.
또한, ‘부정’을 이용해서 증명하는 방법인 “대우” 와 “귀류법” 의 이해도 매우 중요합니다.
대우는 얼핏 보기에도 복잡해 보이는 명제를 단순화하고, 귀류법은 정면 돌파로는 증명할 수 없는(하기 어려운) 명제를 증명할 때 큰 힘을 발휘합니다.
명제란 무엇인지 먼저 확인해 봅시다.
명제: 참과 거짓을 객관적으로 판정할 수 있는 문장이나 식
예를 들어 ‘백두산은 한국에서 가장 높은 산이다’는 명제지만 ‘백두산은 멋있다’는 명제가 아닙니다.
백두산의 높이가 한국에서 가장 높은지는 객관적으로 판정할 수 있지만, 백두산이 멋지다고 느끼는 데는 개인차가 있으며(심지어 대부분이 ‘멋지다’고 생각할지라도) 참과 거짓을 객관적으로 판단할 수 없기 때문입니다.
💡 논리적 사고의 기초: 필요조건과 충분 조건.
우선 필요조건과 충분조건의 정의를 살펴보겠습니다.
필요조건과 충분조건의 정의
명제 ‘P이면 Q이다’ 가 참일 때,
P를(Q이기 위한) “충분조건”
Q를(P이기 위한) “필요조건”
이라고 합니다.
‘P이면 Q이다’ 에서 P를 ‘재즈’라 하고 Q를 ‘음악’이라 하면 ‘재즈는 음악이다’가 됩니다.
이는 당연히 참이므로(올바르므로) 정의에 따라
재즈: 충분조건
음악: 필요조건
이 됩니다.
확실히 재즈가 되기 위해서는 (적어도) 음악일 필요가 있습니다.
또한, 음악이 되기 위해서 재즈면 (넉넉하게) 충분하다고 할 수 있습니다.
재즈는 음악의 한 장르이므로 이 둘의 관계를 그림으로 표현하면 다음과 같은 모습이 됩니다.
이처럼 한쪽이 다른 한쪽을 완전히 포함하는 경우를 다음과 같이 집합으로 이해해 보는 것도 매우 중요합니다.
영역이 더 작은 쪽(재즈): 충분조건
영역이 더 큰 쪽(음악): 필요조건
특히 두 개의 명제 ‘P이면 Q다’ 와 ‘Q이면 P다’가 모두 참일 경우에는 ‘P와 Q가 서로의 필요충분조건이다’라고 합니다. 또는 ‘P와 Q는 서로 동치다’ 라고도 합니다.
NOTE : 실수란
수학에서는 일반적으로 부등식의 범위를 수직선에 나타낼 때,
등호 없는 부등호(<)는 ○와 대각선
등호 있는 부등호(≤)는 ●와 (직선에) 수직으로 뻗은 선
으로 표기합니다.
예를 들어 1 ≤ x < 4는 다음과 같이 표기합니다.
우리는 보통 무언가를 고를 때, 자연스레 “필요저건에 따라 후보를 줄여 나갑니다. 그리고 충분조건을 만족하는 후보를 탐색합니다.”
예를 들어 점심 메뉴를 고를 때, ‘8,000원 안팍의 메뉴’처럼 예산이 필요조건이 사람이 적지 않을 것입니다.
거기에 ‘30분 안에 먹을 수 있는 메뉴’ 혹은 ‘깔끔한 맛’ 등의 필요조건을 더 해, 그 모든 필요조건을 만족하는 메뉴로 후보를 줄여 나갑니다.
그리고 남은 메뉴(후보)가 오늘 점심으로 괜찮은지(충분한지) 고민합니다.
그 결과(예를 들어) ‘그럼 오늘 점심은 경양식 돈까스로 하자’고 결정하는 사고방식은 매우 당연하다고 생각할 것입니다.
이처럼 필요조건과 충분조건을 구분하는 능력은 문제를 해결할 때 대단한 위력을 발휘합니다.
-
-
-
📝[blog post] 연습 문제 풀이 정리(2)
1️⃣ 수열과 재귀.
연습 문제를 풀다보니 수열과 재귀에 대해 많은 수학적 사고력이 필요하겠다는 생각이 들었습니다.
수열 : 수학에서 수의 나열을 의미합니다.
즉, 어떤 규착에 따라 나열된 수들의 집합을 말합니다.
수열은 각 수를 나타내는 일련의 할(terms)으로 구성되며, 각 항은 특정 위치(index)를 가집니다.
수열의 예로는 다음과 같은 것들이 있습니다.
등차수열: 각 항이 일정한 값만큼 증가하거나 감소하는 수열(예: 2, 5, 8, 11…)(각 항이 3씩 증가)
등비수열: 각 항이 일정한 비율로 증가하거나 감소하는 수열(예: 3, 9, 27, 81…)(각 항이 이전 항의 3배)
피보나치 수열: 첫 두 항이 0과 1이고, 그 이후의 각 항이 바로 앞 두항의 합인 수열(예: 0, 1, 1, 2, 3, 5, 8….)
수열은 다양한 수학적 문제를 해결하는 데 사용되며, 특히 함수, 극한, 미적분 등의 주제와 밀접한 관련이 있습니다.
재귀 : 프로그래밍과 수학에서 사용되는 개념으로, 어떤 함수나 알고리즘이 자기 자신을 호출하는 방식울 말합니다.
재귀를 통해 복잡한 문제를 더 작은 하위 문제로 나누어 해결할 수 있습니다.
재귀 함수는 기본적으로 두 가지 부분으로 구성됩니다.
1. 기저 조건(Base Case) : 재귀 호출이 더 이상 필요하지 않은 경우를 정의합니다. 기저 조건이 충족되면 함수는 더 이상 자기 자신을 호출하지 않고 종료됩니다.
2. 재귀 호출(Recursive Call) : 함수가 자기 자신을 호출하여 문제를 더 작은 부분으로 나누어 해결하려고 시도합니다.
재귀는 문제를 단순하고 직관적으로 표현할 수 있는 강력한 도구이지만, 재귀 호출이 과도하면 스택 오버플로(stack overflow)가 발생할 수 있으므로 주의가 필요합니다.
따라서 재귀를 사용할 때는 기저 조건을 잘 정의하고, 필요할 경우 반복(iteration)으로 문제를 해결하는 방법도 고려해야 합니다.
-
-
-
📝[blog post] 연습 문제 풀이 정리(1)
1️⃣ 이중 for 문.
이중 for 문은 for 문을 중첩해서 사용하는 것을 말합니다.
한 for 문 안에 또 다른 for 문 안에 또 다른 for 문이 들어있는 구조로, 주로 2차원 배열이나 리스트, 행렬을 처리할 때 사용됩니다.
1.1 기본 구조.
for (초기화1; 조건1; 증감1) {
for (초기화2; 조건2; 증감2) {
// 코드 블록
}
}
1.2 예시
예를 들어, 2차원 리스트의 모든 요소를 출력하는 경우를 생각해 봅시다.
public class Main {
public static void main(Stringp[] args) {
int[][] matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
for (int[] row : matrix) {
for (int element : row) {
System.out.println(element);
}
}
}
}
위 코드에서 ‘matrix’ 는 2차원 리스트입니다.
첫 번째 for 문은 ‘matrix’ 의 각 행(row)을 순회하고, 두 번째 for 문은 각행의 요소(element)를 순회합니다.
출력 결과는 다음과 같습니다.
1
2
3
4
5
6
7
8
9
2️⃣ 규칙성을 찾는 것이 중요!
어떤 문제를 마주치면 규칙성을 찾는 것이 중요한 것 같습니다.
연습 문제 2-1 중 ‘정수형 숫자를 로마 숫자 표기로 변환하는 프로그램’ 을 작성하는 문제에서 그것을 깨달았습니다.
먼저 어떤 규칙성이 있는지 찾아낸 후 그 규칙성에 따라 문제를 풀고, 문제를 컴퓨터적 사고력을 이용하여 코딩을 하니 문제가 풀리는 것을 알게 되었습니다.
3️⃣ 인덱스를 자유자재로 가지고 놀 줄 알아야 합니다!
연습 문제를 풀면서 느낀 점 중 하나가 “인덱스를 자유자재로 가지고 놀 줄 알아야 한다” 는 부분이었습니다.
“인덱스를 자유자재로 가지고 논다” 라는 말은 문자열이 주어지면 인덱스를 활용하여 문자를 삽입, 삭제, 추출, 변환 등을 자유롭게 할 줄 알아야 한다는 의미입니다.
연습 문제 중 문자열에 대한 문제는 이 부분이 가장 중요시되는 것 같았습니다.
-
-
☕️[Java] 스트림
1️⃣ 스트림.
1. 스트림(Stream)
자바에서 스트림(Stream) API는 자바 8에서 도입되어 컬렉션의 요소를 선언적으로 처리할 수 있는 방법을 제공합니다.
스트림 API를 이용하면 데이터 요소의 시퀀스를 효율적으로 처리할 수 있으며, 데이터를 병렬로 처리하는 것도 간단할게 할 수 있습니다.
스트림을 이용하면 복잡한 데이터 처리 작업을 간결하고 명확한 코드로 작성할 수 있습니다.
1.2 스트림의 주요 특정.
1. 선언적 처리 : 스트림을 사용하면 무엇을 할 것인지(what)에 집중하여 작업을 설명할 수 있고, 어떻게 처리할 것인지(how)는 스트림 API가 알아서 최적화하여 처리합니다.
2. 파이프라이닝 : 스트림 연산은 파이프라인을 형성할 수 있으며, 여러 단계의 처리 과정을 연결하여 복잡한 데이터 처리를 효과적으로 할 수 있습니다.
3. 내부 반복 : 스트림은 “내부 반복”을 사용합니다. 즉, 데이터를 어떻게 반복할지 스트림이 처리하므로, 개발자는 각 요소에 어떤 처리를 할지만 정의하면 됩니다.
4. 불변성 : 스트림은 데이터를 수정하지 않습니다. 대신, 각 단계에서 결과를 내는 새로운 스트림을 생성합니다. 이는 함수형 프로그래밍의 특성을 반영합니다.
1.3 스트림의 작업 흐름.
스트림 API의 작업 흐름은 크게 세 부분으로 나눌 수 있습니다.
1. 스트림 생성 : 컬렉션, 배열, I/O 자원 등의 데이터 소스로부터 스트림을 생성합니다.
List<String> myList = Arrays.asList("a1", "a2", "b1", "b2", "c2", "c1");
Stream<String> myStrean = myList.stream();
2. 중간 연산(Intermediate operations) : 스트림을 변환하는 연산으로, 필터링, 매핑, 정렬 등이 있으며, 이 연산들은 연결 가능하고, 또한 게으르게(lazily) 실행됩니다.
myStream.filter(s -> s.startsWith("c"))
.map(String::toUpperCase)
.sorted();
3. 종단 연산(Terminal operations) : 스트림의 요소들을 소모하여 결과를 생성하는 연산입니다. 예를 들어, forEach, reduce, collect 등이 있으며, 이 연산을 수행한 후 스트림은 더 이상 사용할 수 없습니다.
myStream.forEach(System.out::println);
1.4 스트림과 병렬 처리.
스트림 API는 병렬 처리를 간단하게 지원합니다.
‘paralleStream()’ 을 호출하면 자동으로 여러 쓰레드에서 스트림 연산이 병렬로 수행됩니다.
이는 데이터가 큰 경우에 유용하며, 멀티코어 프로세서의 이점을 쉽게 활용할 수 있게 해줍니다.
1.5 📝 정리.
스트림은 자바에서 데이터 컬렉션을 함수형 스타일로 쉽게 처리할 수 있게 하는 강력한 도구입니다.
이는 코드의 간결성과 가독성을 높이는 데 큰 도움을 줍니다.
2. 중개 연산(Intermediate operations)
자바 스트림 API에서 중개 연산(Intermediate operations)은 스트림의 요소들을 처리하고 변형하는 연산들로서, 다른 스트림을 반환합니다.
중개 연산은 게으른(lazy) 특성을 가지며, 종단 연산(Terminal operation)이 호출되기 전까지는 실제로 실행되지 않습니다.
이런 특성은 연산의 체인을 구성할 때 성능 최적화에 도움을 줍니다.
2.1 중개 연산의 주요 특성.
게으른 실행(Lazy Execution) : 중개 연산은 호출되었을 때 즉시 실행되지 않습니다. 대신, 종단 연산이 호출될 때 까지 실행이 지연됩니다.
스트림 변환 : 각 중개 연산은 변형된 형태의 새로운 스트림을 변환합니다. 이는 연산을 연쇄적으로 연결할 수 있도록 합니다.
2.2 주요 중개 연산의 종류.
1. 필터링(Filtering)
‘filter(Predicate<T> predicate)’ : 주어진 조건(프리디케이트)에 맞는 요소만을 포함하는 스트림을 반환합니다.
List<String> names = Arrays.asList("Jo", "Lee", "Park", "Kang");
names.stream()
.filter(name -> name.startsWith("K"))
.forEach(System.out::println); // 출력: "Kang"
2. 매핑(Mapping)
‘map(Function<T, R> mapper)’ : 스트림의 각 요소에 주어진 함수를 적용하고, 함수 결과로 주성된 새 스트림을 반환합니다.
‘flatMap(Function<T, Stream<R>> mapper)’ : 각 요소에 함수를 적용한 결과로 생성된 여러 스트림을 하나의 스트림으로 평탄화합니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
numbers.stream()
.map(number -> number * number)
.forEach(System.out::println); //출력: 1, 4, 9, 16
3. 정렬(Sorting)
‘sorted() :’ 자연 순서대로 스트림을 정렬합니다.
‘sorted(Comparator<T> comparator) :’ 주어진 비교자를 사용하여 스트림을 정렬합니다.
List<String> fruits = Arrays.asList("banana", "apple", "orange", "kiwi");
fruits.stream()
.sorted()
.forEach(System.out::println); // 출력: apple, banana, kiwi, orange
4. 제한(Limiting) 및 건너뛰기(Skipping)
‘limit(long maxSize)’ : 스트림의 요소를 주어진 크기로 제한합니다.
‘skip(long n)’ : 스트림의 처음 n개 요소를 건너뜁니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.stream()
.skip(2)
.limit(3)
.forEach(System.out::println); // 출력 3, 4, 5
5. 중복 제거(Distinct)
‘distinct()’ : 스트림에서 중복된 요소를 제거합니다.
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5);
numbers.stream()
.distinct()
.forEach(System.out::println); // 출력: 1, 2, 3, 4, 5
2.3 📝 정리.
중개 연산을 통해 데이터 스트림을 세밀하게 제어하고 원하는 형태로 데이터를 변형 할 수 있습니다.
이러한 연산들은 다양한 데이터 처리 작업에서 매우 유용하게 사용됩니다.
3. 최종 연산(Terminal operations)
자바 스트림 API에서 최종 연산(Terminal operations)은 스트림 파이프라인의 실행을 트리거하고 스트림의 요소를 소비하여 결과를 생성하거나 부작용(side effect)을 일으키는 연산입니다.
최종 연산이 호출되기 전까지 중간 연산들은 게으른(lazy) 방식으로 처리되며 실행되지 않습니다.
최종 연산 후에는 스트림이 소비되어 더 이상 사용할 수 없게 됩니다.
3.1 최종 연산의 주요 유형.
1. 수집(Collection)
‘collect(Collector<T, A, R> collector)’ : 스트림의 요소를 변환, 결합하고 컬렉션으로 또는 다른 형태로 결과를 수집합니다.
예를 들어, ‘toList()’, ‘toSet()’, ‘toMap()’ 등이 있습니다.
List<String> names = Array.asList("Alice", "Bob", "Charlie", "David");
List<String> list = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println(list); // 출력 ["Alice"]
2. 집계(Aggregation)
‘count()’ : 스트림의 요소 개수를 반환합니다.
‘max(Comparator<T> comparator)’ : 스트림에서 최대값을 찾습니다.
‘min(Comparator<T> comparator)’ : 스트림에서 최소값을 찾습니다.
‘reduce(BinaryOperator<T> accumulator)’ : 스트림의 요소를 결합하여 하나의 결과를 생성합니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println(sum); // 출력: 10
3. 반복(Iteration)
‘forEach(Consumer<T> action)’ : 각 요소에 대해 주어진 작업을 수행합니다. 스트림의 순서대로 실행됩니다.
List<String> names = Arrays.asList("Alist", "Bob", "Charlie", "David");
names.stream()
.forEach(System.out::println); // Alice, Bob, Charlie, David
4. 조건 검사(Checking)
‘allMatch(Predicate<T> predicate) :’ 모든 요소가 주어진 조건을 만족하는지 검사합니다.
‘anyMatch(Predicate<T>predicate) :’ 어떤 요소라도 주어진 조건을 만족하는지 검사합니다.
‘noneMatch(Predicate<T>predicate) :’ 모든 요소가 주어진 조건을 만족하지 않는지 검사합니다.
boolean allEven = numbers.stream()
.allMatch(n -> n % 2 == 0);
System.out.println(allEven); // 출력: false
5. 요소 검색(Finding)
‘findFirst()’ : 스트림의 첫 번째 요소를 Optional로 반환합니다.
‘findAny()’ : 스트림에서 임의의 요소를 Optional로 반환합니다. 병렬 스트림에서 유용합니다.
Optional<String> first = names.stream()
.findFirst();
first.ifPresent(System.out::println); // 출력 Alice
3.2 📝 정리.
이러한 최종 연산들은 스트림 처리를 완료하고 필요한 결과를 도출하기 위해 사용됩니다.
스트림 API를 통해 데이터 처리를 선언적이고 간결하게 할 수 있으며, 복잡한 로직을 효과적으로 관리할 수 있습니다.
-
-
☕️[Java] 람다식은 하나만!
람다식은 하나만!😆
자바에서는 “하나의 추상 메소드를 갖는 인터페에스에 대해서만 람다식을 직접 사용할 수 있습니다.”
이를 함수형 인터페이스라고 부르며, 람다식은 이런 함수형 인터페이스의 구현을 간단히 할 수 있는 방법을 제공합니다.
하지만 아래의 코드와 같이 인터페이스 내에 두 개의 추상 메서드 (‘plus’, ‘minus’)가 있기 때문에, 이 인터페이스를 람다식으로 직접 구현하는 것은 불가능합니다.
interface Carculator {
public abstract int plus(int x, int y);
public abstract int minus(int x, int y);
}
람다식을 사용하려면 함수형 인터페이스가 필요하므로, 두 메소드 각각을 위한 두 개의 별도의 인터페이스를 정의하거나 기존 인터페이스 중 하나를 수정해야 합니다.
아래의 코드는 이를 위해 각 메소드를 분리하여 두 개의 함수형 인터페이스를 만든 예시입니다.
interface Calculator {
public abstract int operation(int x, int y);
}
public class Main {
public static void main(String[] args) {
Calculator plus = (x, y) -> { return x + y; };
System.out.println(plus.operation(10,2)); // 12
Calculator minus = (x, y) -> { return x - y; };
System.out.println(minus.operation(10,2)); // 8
}
}
위 코드는 각 연산을 람다식으로 간단히 구현하고 있습니다.
만약 원래의 ‘Carculator’ 인터페이스를 유지고하고 싶다면 이를 직접적으로 람다식으로 구현할 수는 없으며, 대신 익명 클래스나 정규 클래스를 사용해야 합니다.
아래의 코드는 익명 클래스를 사용하는 방법을 보여줍니다.
Calculator calclator = new Calculator() {
@Override
public int plus(int x, int y) {
return x + y;
}
@Override
public int minus(int x, int y) {
return x - y;
}
}
-
☕️[Java] 람다식
1️⃣ 람다식.
1. 람다 표현식(Lambda Expression)
자바 프로그래밍에서 람다식 또는 람다 표현식(Lambda Expression)은 간결한 방식으로 익명 함수(anonymous function)를 제공하는 기능입니다.
자바 8부터 도입된 이 기능은 함수형 프로그래밍의 일부 개념을 자바에 도입하여, 코드를 더 간결하고 명료하게 만들어 주며 특히 컬렉션의 요소를 처리할 때 유용하게 사용됩니다.
1.2 람다식의 특징.
익명성 : 람다는 이름이 없기 때문에 익명으로 처리됩니다.
함수 : 람다는 메서드와 유사하지만, 독립적으로 존재할 수 있는 함수입니다.
전달성 : 람다 표현식은 메서드 인자로 전달되거나 변수에 저장될 수 있습니다.
간결성 : 코드의 간결성을 높여, 불필요한 반복을 줄여줍니다.
1.3 람다 표현식의 기본 구조.
람타 표현식은 주로 매개 변수를 받아들여 결과를 반환하는 식의 형태로 작성됩니다.
일반적인 형태는 다음과 같습니다.
(parameters) -> expression
또는
(parameters) -> { statements; }
매개 변수 : 괄호 안에 정의되며, 매개 변수의 타입을 명시할 수도 있고 생략할 수도 있습니다.
매개 변수가 하나뿐인 경우, 괄호도 생략할 수 있습니다.
화살표(->) : 매개 변수와 몸체를 구분짓는 역할을 합니다.
몸체 : 람다의 실행 로직을 담고 있으며, 식(expression) 또는 문장(statements)이 올 수 있습니다.
식은 단일 실행 결과를 반환하며, 중괄호는 생략할 수 있습니다.
문장은 중괄호 안에 작성되며, 여러 줄의 코드를 포함할 수 있습니다.
1.4 예시
Thread 실행하기
new Thread(() -> System.out.println("Hello from a thread")).start();
리스트의 각 요소 출력하기
List<String> list = Arrays.asList("Apple", "Banana", "Cherry");
list.forEach(item -> System.out.println(item));
Comparator를 통한 정렬
List<String> cities = Arrays.asList("Seoul", "New York", "London");
Collections.sort(cities, (s1, s2) -> s1.compareTo(s2));
1.5 📝 정리.
람다 표현식은 이벤트 리스너, 스레드의 실행 코드 등 여러 곳에서 기존에 익명 클래스를 사용하던 부분을 대체하여 코드를 더 간결하게 만들 수 있습니다.
또한, 스트림 API와 함께 사용될 때 강력한 데이터 처리 기능을 제공하여 복잡한 컬렉션 처리를 단순화시킬 수 있습니다.
2. 람다식의 장점.
자바에서 람다식(Lambda Expression)을 사용하는 것은 여러 가지 장점을 제공합니다.
이러한 장점들은 프로그래밍 스타일, 코드의 간결성, 효율성 및 기능성 측면에서 특히 두드러집니다.
2.1 람다식의 주요 장점들.
1. 코드의 간결성 : 람다식을 사용하면 복잡한 익명 클래스를 사용할 필요가 없어지므로 코드를 훨씬 간결하게 작성할 수 있습니다.
예를 들어, 스레드를 실행하거나 이벤트 리스너를 설정할 때 몇 줄의 코드로 작성할 수 있습니다.
2. 가독성 향상 : 람다식은 기존의 익명 클래스보다 훨씬 읽기 쉽고 이해하기 쉬운 코드를 작성할 수 있게 합니다.
이는 유지보수 시간을 줄이고 코드의 질을 향상시키는 데 도움이 됩니다.
3. 함수형 프로그래밍 지원 : 람다식은 자바에 함수형 프로그래밍 스타일을 도입하였습니다.
이는 데이터 처리와 조작을 보다 선언적으로 표현할 수 있게 해, 복잡한 데이터 처리 로직을 간결하고 효율적으로 작성할 수 있도록 합니다.
4. 코드의 재사용성 증가 : 람다식을 사용하면 특정 동작을 수행하는 코드 블록을 쉽게 재사용할 수 있습니다.
람다식은 변수처럼 취급될 수 있어, 메소드에 인자로 전달하거나 변수에 할당하여 다른 위치에서 사용할 수 있습니다.
5. 병렬 실행 용이 : 자바 8 이후로 스트림 API와 결합된 람다식은 컬렉션 처리를 병렬로 쉽게 수행할 수 있게 해줍니다.
이는 ‘parallelStream()’ 과 같은 메소드를 사용하여 멀티코어 프로세서의 이점을 쉽게 활용할 수 있게 합니다.
6. 지연 실행(Lazy Evaluation) : 람다식은 지연 실행을 가능하게 합니다.
예를 들어, 조건이 충족될 때까지 코드 실행을 지연시키거나, 필요할 때만 데이터를 처리하도록 할 수 있습니다.
이는 성능 최적화에 도움을 줄 수 있습니다.
7. 함수 인터페이스와의 호환성 : 람다식은 단일 추상 메소드를 가진 인터페이스(함수 인터페이스)와 호환됩니다.
이는 많은 내장 함수 인터페이스(‘Runnable’, ‘Callable’, ‘Comparator’ 등)와 사용자 정의 함수 인터페이스에 람다식을 적용할 수 있음을 의미합니다.
2.2 📝 정리.
이러한 장점들로 인해 람다식은 자바 프로그래머들 사이에서 매우 인기 있는 기능이 되었으며, 모던 자바 코드에서는 필수적인 요소로 자리 잡고 있습니다.
3. 람다식의 단점.
자바에서 람다식을 사용하면 여러 가지 장점이 있지만, 몇 가지 단점 또는 주의할 점도 있습니다.
3.1 람다식의 사용과 관련된 단점.
1. 디버깅의 어려움 : 람다식은 익명 함수이기 때문에 디버깅이 더 복잡할 수 있습니다.
스택 트레이스에서 람다식이 어디에 위치하는지 명확하게 표시되지 않아 오류를 추적하기 어려울 수 있습니다.
2. 코드의 남용 : 람다식을 과도하게 사용하면 코드가 오히려 더 복잡해지고 이해하기 어려워질 수 있습니다.
특히 람다 내부에 긴 로직이나 조건문을 넣을 경우 가독성이 떨어질 수 있습니다.
3. 람다 캡처링 오버헤드 : 람다식은 주변 환경의 변수를 캡처(Capture)할 수 있습니다.
이 때, 람다가 외부 변수를 캡처 할 경우 추가적인 비용이 발생할 수 있으며, 이는 성능에 영향을 줄 수 있습니다.
4. 직렬화의 문제 : 람다식은 기본적으로 직렬화가 보장되지 않습니다.
따라서 람다식을 사용하는 객체를 직렬화하려고 할 때 문제가 발생할 수 있습니다.
이는 분산 시스템에서 특히 중요한 이슈가 될 수 있습니다.
5. 학습 곡선 : 자바 8 이전의 버전에 익숙한 개발자들에게 람다식과 스트림 API는 새로운 패러다임을 익혀야 하는 도전과제를 제시합니다.
이는 초기 학습 곡선을 가파르게 만들 수 있습니다.
6. 타입 추론의 복잡성 : 람다식에서는 자바 컴파일러가 타입을 추론해야 하는데, 때때로 이 추론이 개발자의 의도와 다른게 이루어질 수 있습니다.
이는 코드의 명확성을 떨어뜨릴 수 있습니다.
7. 함수형 인터페이스의 제약 : 람다식은 단 하나의 추상 메소드를 가진 함수형 인터페이스와 함꼐 사용됩니다.
때로는 이런 제약이 프로그램 설계를 더 복잡하게 만들 수 있습니다.
3.2 📝 정리.
람다식의 단점들은 주로 개발과 관련된 트레이드오프와 관련이 있으며, 이러한 단점을 이해하고 적절히 관리한다면 람다식을 효과적으로 사용할 수 있습니다.
-
☕️[Java] HashMap에 key 값은 항상 int 여야 할까요?
🤔 HashMap에 key 값은 항상 int 여야 할까요?
강의와 예제 코드를 열심히 보고 따라서 타이핑하고 있던 중 “문뜩!” 떠올랐습니다. 🤩
‘HashMap에 key 값은 항상 int 여야 할까요?🤔’
그래서 구글링과 챗 지피티 그리고 Java의 정석 도서를 살펴본 후 이 글을 쓰게 되었습니다 :)
🙅♂️ 대답은 “아니오!” 입니다.
자바 프로그래밍에서 ‘HashMap’ 의 키 값은 ‘int’ 형일 필요는 없다고 합니다.
‘HashMap’ 은 키로서 어떠한 객체도 사용할 수 있으며, 기는 자바의 ‘제네릭’ 을 통해 다양한 유형의 객체를 키로 사용할 수 있게 해준다고 합니다.
(오! “제네릭” 은 아직 안배웠지만 🥲 Swift에서 봐서 비슷한 느낌 같은데?!)
키 객체는 ‘Object’ 클래스의 ‘hashCode()’ 메소드와 ‘equals()’ 메소드를 적절히 구현해야 합니다.
(‘Object’ 클래스는 무엇이고, ‘hashCode()’ 메소드와 ‘equals()’ 메소드는 무엇인가?!! 🤪)
이는 ‘HashMap’ 이 키의 해시 코드를 사용하여 데이터를 저장하고 검색하기 때문입니다.
(도통 무슨 소리인지 몰라서 아래 “제네릭”. “Object 클래스”, “hashCode()”, “equals()”를 정리했어요 ㅎㅎ)
‘HashMap’ 을 사용할 때, 키로 사용되는 객체의 ‘hashCode()’ 메소드가 효율적이고 일관성 있는 값을 반환해야 합니다.
또한, ‘equalse()’ 메소드는 객체의 동등성을 정확하게 판단할 수 있어야 합니다.
이 두 메소드의 구현이 적절히 이루어져야 ‘HashMap’ 이 키의 중복 없이 정확하게 데이터를 관리할 수 있습니다.
예시 - String 객체를 키로 사용하는 ‘HashMap’
import java.util.HashMap;
public class Example {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
map.put("one", 1);
map.put("two", 2);
map.put("three", 3);
System.out.println(map.get("two")); // 출력: 2
}
}
위 예시에서 보듯, ‘String’ 외에도 사용자가 정의한 어떠한 객체든 ‘hashCode()’ 와 ‘equals()’ 가 적절히 구현되어 있다면 키로 사용할 수 있습니다.
따라서 ‘int’ 만을 키로 사용해야 하는 것은 아닙니다.
1️⃣ 제네릭(Generic).
자바에서 ‘제네릭(Generic)’ 은 클래스, 인터페이스, 메소드를 정의할 때 타입(Type)을 하나의 매개변수처럼 취급하여, 다양한 데이터 타입을 사용할 수 있도록 하는 프로그래밍 기법입니다.
제네릭을 사용하면 컴파일 시점에 타입 안정성을 제공하고, 타입 캐스팅을 줄여 코드를 더 간결하고 읽기 쉽게 만들 수 있습니다.
제네릭 기본 문법.
제네릭은 타입 매개변수를 사용하여 구현됩니다.
타입 매개변수는 보통 한 글자로 표현되며, 일반적으로 다음과 같은 문자를 사용합니다.
‘E’ : Element(컬렉션에서 사용되는 요소)
‘K’ : Key(키)
‘V’ : Value(값)
‘T’ : Type(일반적인 타입)
‘S’, ‘U’, ‘V’ 등 - 두 번째, 세 번째, 네 번째 타입을 나타내기 위해 사용
예시: 제네릭을 사용한 클래스와 메소드
// 제네릭 클래스 예시
public class Box<T> {
private T t; // T는 이 클래스가 다루는 객체의 타입을 매개변수화합니다.
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
// 제네릭 메소드 예시
public static <T> void printArray(T[] inputArray) {
for (T element : inputArray) {
System.out.print(element + " ");
}
System.out.println();
}
위 예시에서 ‘Box’ 클래스는 타입 매개변수 ‘T’ 를 사용하여 다양한 타입을 저장하고 반환할 수 있는 범용 컨테이너로 사용됩니다.
‘printArray’ 메소드는 어떤 배열 타입도 받아들일 수 있으며, 그 요소들을 출력합니다.
2️⃣ Object 클래스.
자바 프로그래밍에서 ‘Object’ 클래스는 자바의 클래스 계층 구조에서 가장 상위에 위치하는 클래스입니다.
모든 자바 클래스는 직접적이거나 간접적으로 ‘Object’ 클래스를 상속받습니다.
이는 ‘Object’ 클래스가 자바에서 모든 클래스의 근본(base)이라는 의미 입니다.
‘Object’ 클래스는 자바의 ‘java.lang’ 패키지에 포함되어 있으며, 모든 객체에 공통적으로 필요한 메서드를 제공합니다.
Object 클래스의 의의.
‘Object’ 클래스의 메서드들은 자바의 모든 클래스에 기본적인 기능을 제공합니다.
이로 인해, 개발자는 어떤 클래스를 만들 때도 이러한 기본적인 메서드들을 새로 작성하지 않고도, 필요에 따라 이를 상속받아 확장하거나 재정의할 수 있습니다.
‘Object’ 클래스는 자바의 모든 클래스와 객체에 공통적인 근복적인 메커니즘을 제공하는 중추적인 역할을 합니다.
3️⃣ Object 클래스의 hashCode() 메소드.
자바의 ‘Object’ 클래스에서 ‘hashCode()’ 메소드는 객체의 메모리 주소를 기반으로 계산된 정수 값을 반환하는 메소드입니다.
이 메소드는 객체의 해시 코드를 제공하며, 해시 기반 컬렉션(예: ‘HashMap’, ‘HashSet’, ‘Hashtable’ 등)에서 객체를 효율적으로 관리하기 위해 사용됩니다.
hashCode() 메소드의 주요 용도
1. 해시 테이블 사용 : ‘hashCode()’ 는 특히 해시 테이블을 사용하는 자료 구조에서 중요합니다.
객체의 해시 코드를 사용하여, 해당 객체가 저장되거나 검색될 해시 버킷을 결정합니다.
이로 인해 데이터의 삽입, 검색, 삭제 작업이 빠르게 수행될 수 있습니다.
2. 객체의 동등성의 빠른 검증 : ‘hashCode()’ 메소드는 ‘equals()’ 메소드와 함께 사용되어 객체의 동등성을 검사합니다.
두 객체가 같다면 반드시 같은 해시 코드를 반환해야 합니다.
따라서, 해시 코드가 다른 두 객체는 결코 같을 수 없으므로, ‘equals()’ 호출 전에 해시 코드를 먼저 확인함으로써 불필요한 비교를 줄일 수 있습니다.
4️⃣ Object 클래스의 equals() 메소드.
자바 프로그래밍에서 ‘Object’ 클래스의 ‘equals()’ 메소드는 두 객체가 동등한지 비교하는데 사용됩니다.
이 메소드는 ‘Object’ 클래스에서 모든 클래스로 상속되며, 특히 객체의 동등성을 판단할 때 중요한 역할을 합니다.
기본적으로, ‘Object’ 클래스의 ‘equals()’ 메소드는 두 객체의 참조가 같은지 확인합니다.
즉, 두 객체가 메모리상에서 같은 위치를 가리키는지 검사합니다.
-
☕️[Java] 컬렉션 프레임워크
1️⃣ 컬렉션 프레임워크
1. 컬렉션 프레임워크(Collection Framework)
자바 컬렉션 프레임워크는 자료 구조를 효율적으로 관리하고 조작할 수 있는 방법을 제공하는 통합 아키텍처입니다.
이 프레임워크는 다양한 인터페이스와 구현을 포함하며, 다양한 종류의 컬렉션들을 제어하고, 데이터 집합을 효율적으로 관리하기 위한 알고리즘을 제공합니다.
1.2 컬렉션 프레임워크의 구요 구성 요소.
1. 인터페이스(Interface) : 컬렉션 프레임워크의 핵심 인터페이스로는 ‘Collection’, ‘List’, ‘Queue’ 등이 있으며 각각 다른 형태의 데이터 집합을 추상화합니다.
예를 들어, ‘List’ 는 순서가 있는 데이터 집합을, ‘Set’ 은 중복을 허용하지 않는 데이터 집합을 나타냅니다.
2. 구현(Implementation) : 이러한 인터페이스를 실제로 구현한 클래스들로, ‘ArrayList’, ‘LinkedList’, ‘HashSet’, ‘TreeSet’, ‘PriorityQueue’ 등이 포함됩니다.
각 클래스는 컬렉션 인터페이스를 구현하며, 데이터의 특성에 따라 선택하여 사용할 수 있습니다.
3. 알고리즘(Algorithm) : 컬렉션 데이터를 처리하는 데 필요한 다양한 알고리즘이 제공됩니다.
이 알고리즘은 정렬, 검색, 순환 및 변환 등을 포함하며, 이들은 대부분 ‘Collections’ 클래스에 정적 메소드로 제공됩니다.
1.3 📝 정리.
컬렉션 프레임워크를 사용하면 데이터를 보다 효율적으로 처리할 수 있고, 기능의 재사용성 및 유지 보수성이 향상됩니다.
또한, 자바 개발자로서 다양한 데이터 컬렉션을 쉽게 처리하고, 표준화된 방법으로 데이터를 조작할 수 있는 능력을 갖추게 됩니다.
2. List 인터페이스.
자바 프로그래밍에서 ‘List’ 인터페이스는 ‘java.util’ 패키지의 일부로, 순서가 있는 컬렉션을 나타냅니다.
이 인터페이스를 사용하면 사용자가 목록의 특정 위치에 접근, 삽입, 삭제를 할 수 있는 동시에, 목록의 요소들이 입력된 순서대로 저장 및 관리됩니다.
‘List’ 는 중복된 요소의 저장을 허용하기 때문에, 같은 값을 가진 요소를 여러 개 포함할 수 있습니다.
2.1 List 인터페이스의 주요 메서드.
add(E e) : 리스트의 끝에 요소를 추가합니다.
add(int index, E element) : 리스트의 특정 위치에 요소를 삽입합니다.
remove(Object o) : 리스트에서 지정된 요소를 삭제합니다.
remove(int index) : 리스트에서 지정된 위치의 요소를 삭제합니다.
get(int index) : 지정된 위치에 있는 요소를 반환합니다.
set(int index, E element) : 리스트의 특정 위치에 요소를 설정(교체)합니다.
indexOf(Object o) : 객체를 찾고, 리스트 내의 첫 번째 등장 위치를 반환합니다.
size() : 리스트에 있는 요소의 수를 반환합니다.
clear() : 리스트에서 모든 요소를 제거합니다.
2.3 가장 널리 사용되는 구현체.
‘List’ 인터페이스는 다양한 구현체를 가지고 있으며, 가장 널리 사용되는 구현체는 ‘ArrayList’, ‘LinkedList’ 그리고 ‘Vector’ 입니다.
각 구현체는 내부적인 데이터 관리 방식이 다르므로, 사용 상황에 따라 적합한 구현체를 선택할 수 있습니다.
‘ArrayList’ : 내부적으로 배열을 사용하여 요소들을 관리합니다. 인덱스를 통한 빠른 접근이 가능하지만, 크기 조정이 필요할 때 비용이 많이 들 수 있습니다.
‘LinkedList :’ 내부적으로 양방향 연결 리스트를 사용합니다. 데이터의 삽입과 삭제가 빈번하게 일어나는 경우 유용합니다.
Vector : ‘ArrayList’ 와 비슷하지만, 모든 메소드가 동기화되어 있어 멀티스레드 환경에서 사용하기에 안전합니다.
2.4 📝 정리.
이러한 특성들로 인해 ‘List’ 인터페이스는 자바에서 데이터를 순차적으로 처리할 필요가 있는 다양한 애플리케이션에서 중요하게 사용됩니다.
3. Set 인터페이스.
자바 프로그래밍에서 ‘Set’ 인터페이스는 ‘java.util’ 패키지의 일부이며, 중복을 허용하지 않는 요소의 컬렉션을 나타냅니다.
‘Set’ 은 ‘Collection’ 인터페이스를 확장하는 인터페이스로서, 집합의 개념을 구현합니다.
이는 각 요소가 컬렉션 내에서 유일하게 존재해야 함을 의미합니다.
인덱스로 요소를 관리하는 ‘List’ 인터페이스와 달리, ‘Set’ 은 요소의 순서를 유지하지 않습니다.
3.1 Set의 주요 특징.
중복 불허 : 같은 요소의 중복을 허용하지 않으며, 이미 ‘Set’ 에 존재하는 요소를 추가하려고 시도하면 그 요소는 컬렉션에 추가되지 않습니다.
순서 보장 없음 : 대부분의 ‘Set’ 구현체는 요소의 저장 순서를 유지하지 않습니다. 그러나 ‘LinkedHashSet’ 과 같은 특정 구현체는 요소의 삽입 순서를 유지할 수 있습니다.
값에 의한 접근 : ‘Set’ 은 인덱스를 사용하지 않고 값에 의해 요소에 접근합니다.
3.2 주요 메서드.
‘Set’ 인터페이스는 ‘Collection’ 인터페이스에서 상속받은 다양한 메소드를 포함합니다.
주요 메서드는 다음과 같습니다.
add(E e): 요소 e를 Set에 추가합니다. 이미 존재하는 요소를 추가하려는 경우, 요소는 추가되지 않고 false를 반환합니다.
remove(Object o): 지정된 객체 o를 Set에서 제거합니다.
contains(Object o): Set이 지정된 객체 o를 포함하고 있는지 여부를 반환합니다.
size(): Set의 요소 개수를 반환합니다.
isEmpty(): Set이 비어 있는지 여부를 반환합니다.
clear(): Set의 모든 요소를 제거합니다
3.3 주요 구현체.
‘Set’ 인터페이스는 여러 가지 방법으로 구현될 수 있으며, 각 구현체는 다른 특성을 가집니다.
HashSet : 가장 널리 사용되는 ‘Set’ 구현체로, 해시 테이블을 사용하여 요소를 저장합니다. 요소의 삽입, 삭제, 검색 작업은 평균적으로 상수 시간(O(1))이 걸립니다.
LinkedHashSet : ‘HashSet’ 의 확장으로, 요소의 삽입 순서를 유지합니다.
TreeSet : 레드-블랙 트리 구조를 사용하여 요소를 저장합니다. 요소는 자연적 순서 또는 비교자에 의해 정렬됩니다.
이로 인해 삽입, 삭제, 검색 작업에 로그 시간(O(log n))이 걸립니다.
3.4 📝 정리.
‘Set’ 인터페이스는 주로 중복을 허용하지 않는 데이터 컬렉션을 다루는 데 사용되며, 특히 요소의 유일성을 보장하는데 유용합니다.
4. Map 인터페이스.
자바에서 ‘Map’ 인터페이스는 ‘java.util’ 패키지에 속하며, 키(key)와 값(value) 쌍으로 이루어진 데이터를 저장하는 자료구조를 정의합니다.
‘Map’ 은 키의 중복을 허용하지 않으면서 각 키는 하나의 값에 매핑됩니다.
값은 중복될 수 있지만, 각 키는 유일해야 합니다.
이러한 특성 때문에 ‘Map’ 은 키를 통해 빠르게 데이터를 검색할 수 있는 효율적인 수단을 제공합니다.
4.1 Map의 주요 특징.
키 기반 데이터 접근 : 키를 사용하여 데이터에 접근하므로, 키에 대한 빠른 검색, 삽입, 삭제가 가능합니다.
키의 유일성 : 같은 키를 다시 ‘Map’ 에 추가하려고 하면 기존 키에 연결된 값이 새 값으로 대체됩니다.
값의 중복 허용 : 같은 값을 가진 여러 키가 ‘Map’ 에 존재할 수 있습니다.
4.2 주요 메서드
‘Map’ 인터페이스는 데이터를 관리하기 위해 다음과 같은 주요 메소드를 제공합니다.
put(K key, V value): 키와 값을 Map에 추가합니다. 이미 키가 존재하면, 해당 키의 값이 새로운 값으로 업데이트 됩니다.
get(Object key): 지정된 키에 연결된 값을 반환합니다. 키가 존재하지 않는 경우, null을 반환합니다.
remove(Object key): 지정된 키와 그 키에 매핑된 값을 Map에서 제거합니다.
containsKey(Object key): Map에 특정 키가 있는지 검사합니다.
containsValue(Object value): Map에 특정 값이 하나 이상 있는지 검사합니다.
keySet(): Map의 모든 키를 Set 형태로 반환합니다.
values(): Map의 모든 값을 컬렉션 형태로 반환합니다.
entrySet(): Map의 모든 “키-값” 쌍을 Set 형태의 Map.Entry 객체로 반환합니다.
size(): Map에 저장된 “키-값” 쌍의 개수를 반환합니다.
clear(): Map의 모든 요소를 제거합니다.
4.3 주요 구현체
‘Map’ 인터페이스의 주요 구현체로는 다음과 같은 클래스들이 있습니다.
HashMap : 가장 일반적으로 사용되는 ‘Map’ 구현체로, 해시 테이블을 사용합니다.
요소의 순서를 보장하지 않으며, 키와 값에 ‘null’ 을 허용합니다.
LinkedHashMap : ‘HashMap’ 을 상속받아 구현된 클래스로, 요소의 삽입 순서를 유지합니다.
이는 순회 시 삽인된 순서대로 요소를 얻을 수 있게 해줍니다.
TreeMap : 레드-블랙 트리를 기반으로 하는 ‘Map’ 구현체로, 모든 키가 자연적 순서대로 정렬됩니다.
정렬된 순서로의 접근이 필요할 때 유용합니다.
Hashtable : ‘HashMap’ 과 유사하지만, 모든 메소드가 동기화되어 있어 멀티스레드 환경에서 사용하기에 안전합니다.
그러나 성능이 ‘HashMap’ 보다 느리고, 키와 값에 ‘null’ 을 허용하지 않습니다.
4.4 📝 정리.
‘Map’ 인터페이스는 다양한 애플리케이션에서 설정, 프로파일, 사용자 세션 등의 데이터를 키와 값의 형태로 관리할 때 유용하게 사용됩니다.
-
-
☕️[Java] 입출력(2)
1️⃣ 입출력(2)
1. 파일 출력.
자바 프로그래밍에서 파일 출력은 프로그램이 데이터를 쓰는 과정을 말합니다.
이 과정을 통해 프로그램은 실행 결과를 저장하거나, 사용자가 입력한 정보를 파일에 기록하고, 다른 프로그램이나 나중에 프로그램 자체가 다시 사용할 수 있는 형태로 데이터를 출력할 수 있습니다.
2. 파일 출력을 수행하기 위한 기본 방법들.
1. FileOutputStream 사용
‘FileOutputStream’ 클래스는 바이트 단위의 출력을 파일에 직접 쓸 때 사용됩니다.
이 클래스를 사용하면 이미지, 비디오 파일, 이진 데이터 등을 파일로 저장할 수 있습니다.
```java
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputExample {
public static void main(String[] args) {
String data = “Hello, this is a test.”;
try (FileOutputStream out = new FileOutputStream(“output.txt”)) {
out.write(data.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
- **2. PrintWriter 사용**
- **'PrintWriter'** 는 문자 데이터를 출력할 때 사용됩니다.
- 이 클래스는 파일에 텍스트를 쓸 때 편리하며, 자동 플러싱 기능, 줄 단위 출력 등의 메소드를 제공합니다.
```java
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
public class PrintWriteExample {
public static void main(String[] args) {
try (PrintWriter writer = new PrintWriter(new FileWriter("output.txt", true))) {
writer.println("Hello, this is a test.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. FileWriter 사용
‘FileWriter’ 는 자바에서 파일에 텍스트 데이터를 쓰기 위한 간편한 방법 중 하나입니다.
이 클래스는 내부적으로 문자 데이터를 파일에 쓸 수 있도록 ‘OutputStreamWriter’ 를 사용하여 바이트 스트림을 문자 스트림으로 변환합니다.
‘FileWriter’ 는 텍스트 파일을 쉽게 작성할 수 있도록 해주며, 생성자를 통해 다양한 방식으로 파일을 열 수 있습니다.
```java
import java.io.FileWriter;
import java.io.IOException;
public class FileWriterExample {
pulbic static void main(String[] args) {
try (FileWriter writer = new FileWriter(“output.txt”, true)) {
writer.write(“Hello, this is a test.”);
} catch (IOException e) {
e.printStackTrace();
}
}
}
- **4. BufferedWriter 사용**
- **'BufferedWrite'** 는 버퍼링을 통해 효율적으로 파일에 문자 데이터를 쓸 수 있도록 합니다.
- **'FileWriter'** 와 함께 사용되어, 더 큰 데이터를 처리할 때 성능을 개선합니다.
```java
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class BufferedWriterExample {
public static void main(String[] args) {
String content = "Hello, this is a test.";
try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
writer.write(content);
} catch (IOException e) {
e.printStackTrace();
}
}
}
2. 파일 입력.
자바 프로그래밍에서 파일 입력은 프로그램이 파일로부터 데이터를 읽어들이는 과정을 말합니다.
이 데이터는 텍스트나 바이너리 형태일 수 있으며, 파일에서 데이터를 읽어 프로그램 내에서 사용할 수 있도록 만드는 것이 목적입니다.
파일 입력을 위해 자바는 다양한 입출력 클래스를 제공합니다.
2.1 주로 사용되는 파일 입력 방법.
1. FileInputStream 사용
‘FileInputStream’ 은 바이트 단위로 파일에서 데이터를 읽는 데 사용됩니다.
이 클래스는 이미지, 비디오 파일, 실행 파일등의 이진 데이터 처리에 주로 사용됩니다.
```java
import java.io.FileInputStream;
import java.io.IOException;
public class FileInputStreamExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream(“input.dat”)) {
int content;
while ((content = fis.read()) != -1) {
// content 변수에 한 바이트씩 읽어들인 데이터를 저장
System.out.print((char) content);
}
} catch (IOExecption e) {
e.printStackTrace();
}
}
}
- **2. BufferedRead** 와 **FileReader 사용**
- **'BufferedReader'** 와 **'FileReader'** 는 텍스트 데이터를 효과적으로 읽기 위해 함께 사용됩니다.
- **'FileReader'** 는 파일에서 문자 데이터를 읽어들이며, **'BufferedReader'** 는 버퍼링을 통해 읽기 성능을 향상 시킵니다.
```java
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class BufferedReaderExample {
public static void main(String[] args) {
try (BufferedReader br new BufferedReader(new FileReader("input.txt"))) {
String line;
while ((line = br.readline()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. Scanner 사용
‘Scanner’ 클래스는 텍스트 파일을 읽을 때 유용하며, 특히 토큰화(tokenizing)된 데이터를 처리할 때 편리합니다.
‘Scanner’ 는 정규식을 사용하여 입력을 구분자로 분리하고, 다양한 타입으로 데이터를 읽어들일 수 있습니다.
```java
import java.io.File;
import java.util.Scanner;
public class ScannerExample {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(new File(“input.txt”))) {
while (scanner.hasNextLine()) {
System.out.println(scanner.nextLine());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
2.2 📝 정리.
이렇게 다양한 방법을 통해 파일로부터 데이터를 읽을 수 있으며, 각 방법은 사용하는 데이터 타입과 처리할 데이터의 양에 따라 선택할 수 있습니다.
파일에서 데이터를 읽는 것은 데이터를 처리하거나, 설정 정보를 불러오거나, 사용자 데이터를 읽는 등 다양한 목적으로 활용됩니다.
-
☕️[Java] 예외 처리
1️⃣ 예외 처리
예외 처리가 무엇인지 이해하고, 예외 처리 방법에 대해 직접 구현
1. 예외(Exception)
자바 프로그래밍에서 “예외(Exception)” 란 프로그램 실행 중에 발생하는 비정상적인 조건 또는 오류를 의미합니다.
이는 프로그램의 정상적인 흐름을 방해하며, 적절히 처리하지 않으면 프로그램이 비정상적으로 종료될 수 있습니다.
자바에서는 이러한 예외를 효과적으로 처리하기 위해 강력한 예외 처리 메커니즘을 제공합니다.
1.2 예외의 유형.
자바에서 예외는 크게 두 가지 유형으로 나눌 수 있습니다.
1. Checked Execptions
컴파일 시간에 체크되는 예외로, 컴파일러가 해당 예외를 처리하도록 요구합니다.
이 예외들은 주로 외부 시스템과의 상호 작요(파일 입출력, 네트워크 통신 등)에서 발생하며, 프로그래머가 이를 적절히 처리하도록 강제합니다.
2. Unchecked Exceptions
런타임에 발생하는 예외로, 주로 프로그래머의 실수로 인해 발생합니다.(예: 배령의 범위를 벗어나는 접근, null 참조 등.)
이러한 예외는 컴파일러가 체크하지 않으므로, 개발자가 예측하고 적절히 처리할 필요가 있습니다.
2. 예외 처리(Exception Handling)
자바 프로그래밍에서 예외 처리는 프로그램 실행 중에 발생할 수 있는 예외적인 상황, 즉 오류나 문제를 안전하게 관리하고 대처하는 방법을 말합니다.
예외 처리를 통해 프로그램의 비정상적인 종료를 막고, 오류 발생 시 적절한 대응을 할 수 있도록 합니다.
이는 프로그램의 안정성과 신뢰성을 높이는 데 중요한 역할을 합니다.
2.1 예외 처리의 주요 구성 요소.
1. try 블록
예외가 발생할 가능성이 있는 코드를 이 블록 안에 넣습니다.
만약 블록 안의 코드 실행 중에 예외가 발생하면, 즉시 해당 블록의 실행을 중단하고 ‘catch’ 블록으로 제어를 넘깁니다.
2. catch 블록
‘try’ 블록에서 발생한 특정 유형의 예외를 처리합니다.
프로그램이 예외를 안전하게 처리할 수 있도록 적절한 로직을 구현할 수 있습니다.
하나의 ‘try’ 블록에 여러 ‘catch’ 블록을 사용하여 다양한 종류의 예외를 각각 다르게 처리할 수 있습니다.
3. finally 블록
이 블록은 예외의 발생 여부롸 관계없이 실행되는 코드를 포함합니다.
주로 사용되는 목적은 자원 해제와 같은 정리 작업을 수행하기 위함입니다.
예를 들어 파일이나 네트워크 자원을 닫거나 해제할 때 사용됩니다.
4. throws 키워드
메소드 선언 시 사용되며, 해당 메소드가 예외를 직접 처리하지 않고 호출한 메소드로 예외를 전파하겠다는 것을 나타냅니다.
이를 통해 예외 처리의 책임을 메소드 호출자에게 넘길 수 있습니다.
2.2 예외 처리 예제.
public class ExceptionHandlingExample {
public static void main(String[] args) {
try {
int result = divide(10, 0);
System.out.println("Result: " + result);
} catch (ArithmeticException e) {
System.err.println("Arithmetic Exception: Division by zero is not allowed.");
} finally {
System.out.println("This block is always executed.");
}
}
public static int divide(int numerator, in denominator) {
return numerator / denominator; // This ca throw ArithmeticException if denominator is zero.
}
}
이 예제에서 ‘divide’ 메소드는 분모가 0일 때 ArithmeticException 을 발생시킬 수 있습니다.
‘try’ 블록 안에서 이 메소드를 호출하고, 예외가 발생하면 ‘catch’ 블록에서 이를 잡아서 적절한 오류 메시지를 출력합니다.
또한, ‘finally’ 블록은 예외 발생 여부와 상관없이 실행되어 어떤 상황에서도 실행될 필요가 있는 코드를 포함할 수 있습니다.
3. throw 키워드.
자바 프로그래밍에서 ‘throw’ 키워드는 개발자가 의도적으로 예외를 발생시키기 위해 사용합니다.
이를 통해 특정 상황에서 프로그램의 흐름을 제어하거나, 특정 조건에서 오류를 발생시켜 예외 처리 메커니즘을 테스트하거나 강제할 수 있습니다.
‘throw’ 는 예외 객체를 생성하고 이를 던집니다(throw)
즉, 프로그램의 정상적인 실행 흐름을 중단하고 예외 처리 루틴으로 제어를 이동시킵니다.
3.1 ‘throw’ 사용법.
‘throw’ 를 사용할 때는 예외 객체를 생성해야 합니다.
이 객체는 ‘Throwable’ 클래스 또는 그 하위 클래스의 인스턴스여야 합니다.
자바에서는 대부분 ‘Exception’ 클래스 또는 그 서브클래스를 사용하여 예외를 생성하고 던집니다.
예제.
public class Main {
public static void main(String[] args) {
try {
checkAge(17);
} catch (Exception e) {
System.out.println("Exception caught: " + e.getMessage());
}
}
static void checkAge(int age) throws Execption {
if (age < 18) {
throw new Exception("Access denied - You must be at least 18 years old.");
}
System.out.println("Access granted - You are old enough!");
}
}
이 예제에서 ‘checkAge’ 메소드는 나이를 확인하고, 18세 미만인 경우 예외를 던집니다.
이 예외는 ‘throw new Exception(…)’ 을 통해 생성되고 던져집니다.
메인 메소드에서는 이 메소드를 ‘try’ 블록 안에서 호출하고, ‘catch’ 블록을 통해 예외를 잡아서 처리합니다.
결과적으로, 사용자가 18세 미만이면 “Access denided” 메시지를 포함하는 예외가 출력됩니다.
3.2 ‘throw’와 ‘throws’의 차이
‘throw’ : 예외를 실제로 발생시키는 행위입니다. 이는 메소드 내부에서 특정 조건에서 예외를 발생시킬 때 사용됩니다.
‘throws’ : 메소드 선언에 사용되며, 해당 메소드가 실행되는 동안 발생할 수 있는 예외를 명시적으로 선언합니다. 이는 호출자에게 해당 메소드를 사용할 때 적절한 예외 처리가 필요하다는 것을 알립니다.
4. throws 키워드.
자바 프로그래밍에서 ‘throws’ 키워드는 메소드 선언에 사용되며, 해당 메소드가 실행 도중 발생할 수 있는 특정 유형의 예외를 명시적으로 선언하는 데 사용됩니다.
‘throws’ 는 메소드가 예외를 직접 처리하지 않고, 대신 이를 호출한 메소드로 예외를 “던져”(전파하는) 사실을 알립니다.
이를 통해 예외 처리 책임을 메소드 호출자에게 넘기는 것입니다.
4.1 ‘throws’ 사용의 목적.
명시성
메소드가 발생시킬 수 있는 예외를 명시함으로써, 이 메소드를 사용하는 다른 개발자들에게 해당 메소드를 사용할 때 어떤 예외들을 처리해야 하는지 명확하게 알릴 수 있습니다.
강제 예외 처리
‘throws’ 로 선언된 예외는 대부분 “checked exception” 이며, 이는 메소드를 호출하는 코드가 반드시 이 예외들을 처리하도록 강제합니다(try-catch 블록을 사용하거나, 또 다시 ‘throws’ 로 예외를 전파하도록 함).
4.2 ‘throws’ 사용법 예제.
아래 예제에서는 ‘throws’ 를 사용하여 ‘IOException’ 을 처리하는 방법을 보여줍니다.
이 예외는 파일 입출력 작업에서 자주 발생합니다.
import java.io.*;
public class Main {
public static void main(String[] args) {
try {
readFile("example.txt");
} catch (IOExecption e) {
System.out.println("An error occurred: " + e.getMessage());
}
}
public static void readFile(String filename) throws IOException {
File file = new File(filename);
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
int content;
while ((content = fis.read()) != -1) {
// Process the content
System.out.print((char) content);
}
} finally {
if (fis != null) {
fis.close();
}
}
}
}
이 예제에서 ‘readFile’ 메소드는 파일을 읽을 때 발생할 수 있는 IOException 을 처리하지 않고, 대신 ‘throws’ 키워드를 사용하여 이 예외를 메소드를 호출한 ‘main’ 메소드로 전달합니다.
‘main’ 메소드는 이 예외를 ‘catch’ 블록을 통해 처리합니다.
-
-
📝[blog post] 프론트엔드와 백엔드는 무엇이 다를까?(+내가 백엔드 개발자가 되고 싶은 이유)
1️⃣ 프론트엔드와 백엔드?
처음 이 글의 여정을 함께하기에 앞서 프론트엔트가 무엇인가 백엔드가 무엇인지 알아야 할 것 같아요!
제가 아무것도 모르는 당시 저 두 단어 “프론트엔드”, “백엔드”를 듣고 느낀 것은
“프론트엔드”는 뭔가 프론트 데스크 같이 앞에서 누군가가 나를 반겨주는 느낌이였고, “백엔드”는 뒤쪽에서 나를 받쳐주는 든든한 느낌이랄까? 😆
그저 느낌으로는 알쏭달쏭하니 정확한 의미를 알아보는 여행을 떠나봅시다! 🙋♂️
2️⃣ 프론트엔드.
프론트엔드는 웹사이트에서 우리가 볼 수 있는 모든 것들을 만드는 일을 말해요 😆
예를 들어, 컴퓨터나 핸드폰으로 책을 보거나 게임을 할 때, 그 화면에 보이는 모든 것들이 바로 프론트엔드에서 만들어진 거예요.(존경합니다 프론트엔드 개발자님들🙇♂️)
이렇게 생각해 볼까요?
웹사이트를 마치 컬러링북처럼 생각한다면, 프론트엔드 개발자는 그림을 그리고 색칠하는 사람이에요 🧑🎨
프론트엔드 개발자들은 화면에 나타날 모양이나 색상을 정하고, 어디를 누르면 어떤일이 일어날지도 결정합니다.
예를 들어, ‘스타드’ 버튼을 누르면 게임이 시작되거나, 사진을 클릭하면 커지는 것처럼 말이에요.
즉, 프론트엔드는 우리가 웹사이트에서 보고 만지는 모든 것을 아름답고 재미있게 만들어 주는 중요한 일을 한답니다!
3️⃣ 백엔드.
백엔드는 웹사이트에서 우리가 눈에 보이지 않는 부분을 다루는 일을 해요.(그렇다고 뭐.. 해커 이런건 아닙니다.. 완전히 달라요…)
이것은 마치 마술사가 무대 뒤에서 마술을 준비하는 것과 비슷해요! 🪄
우리가 볼 수는 없지만, 마술이 멋기제 보이도록 도와주죠.
예를 들어, 우리가 컴퓨터로 쇼핑을 할 때, 옷이나 장난감을 고르고 주문 버튼을 눌러요. 이떄 백엔드는 주문한 것이 무엇인지 기억하고, 그 물건을 어디로 보내야 할지 알려줘요.
또한, 우리가 어떤 게임을 하거나 질문을 할 때도, 백엔드는 그 대답을 찾아서 화면에 보여주죠.
백엔드는 컴퓨터와 데이터베이스라는 큰 저장소를 사용해서, 우리가 웹사이트에서 필요한 모든 정보를 처리하고 저장하는 곳이에요.
우리가 보지 못하지만, 웹사이트가 잘 작동하도록 도와주는 매우 중요한 부분이랍니다!
4️⃣ 내가 백엔드 개발자가 되고 싶은 이유.
저는 어렸을 때 레고를 참 좋아했어요 :)
그 중에서도 테크닉 레고를 가장 좋아했었어요 :)
그 이유는 완성된 것을 보는 것도 좋았지만 조립해 나가면서 그 안에 중심이 되는 코어, 즉 움직임의 중앙부를 제가 직접 조립하고 움직임이 어디서부터 시작되는지를 직접 이해하는 것이 너무 재미있었거든요.
자동차 레고를 만들다보면 직접 엔진를 만들게 됩니다.
그러면 진짜 엔진이 어떻게 움직이고 이 엔진이 어떻게 동작하느냐에 따라 자동차의 다른 부품들이 맞물려 하나씩 동작하는지 상상되는게 너무 행복했었어요.
이런것들이 어렸을 때부터 너무 좋았답니다.
그리고나서 조금 커서는 루어 낚시를 좋아하게 되었어요.
이 루어 낚시는 “배스” 라는 어종을 대상으로 하는 낚시인데, 이 어종에 대한 여러가지 공부를 해야 했었어요.
먼저, 이 어종이 온도에 민감해 온도에 따라 공격 패턴이 달라요 그래서 그 패턴에 대한 데이터를 수집해야 했었어요.
두 번째, 이 어종은 수중 구조물에 굉장히 예민해요. 자신이 좋아하는 수중 구조물이 따로 있어서 그 수중 구조물을 따로 탐색하고 이해하는 법을 배워야 했었어요.
세 번째, 날씨에 영향을 많이 받는 어종이에요. 햇빛과 그늘 그리고 비가 오는 날과 안오는 날에 따라 먹이 사냥 패턴이 달라져요. 그에 따른 루어 선택과 패턴을 다르게 골라야 합니다.
네 번째, 피딩 타임이라는 이 어종의 먹이 사냥 시간이 있습니다. 이 시간에 따라 어종의 먹이 사냥 패턴이 매우 다양해요.
마지막, 계절에 따라 이 어종이 물 속이 바닥, 중층 또는 상층에 머무는지 이런 데이터가 달라요.
이렇게 이 어종을 낚기 위해서는 수 많은 변수와 데이터들을 조합하여 적절한 위치에 적합한 루어를 선택하여 공격 패턴에 맞는 액션을 주어야 배스가 물어 줍니다.
그럴때 “아 나의 데이터가 맞았구나!” 하는 희열감과 아드레날린 그리고 도파민이 폭발해버리죠.
이런 특성이 저는 백엔드에서도 비슷하게 적용되는 것 같아요.
레고는 백엔드에서의 중심 동작을 알아가는 과정과 직접 동작하는 로직을 만드는 부분에서의 즐거움을 찾아가는 과정에서 재미를 느끼고,
낚시는 백엔드에서 데이터를 찾고 뽑아내어 가공하고 내어주는 부분에서 희열을 느끼는 것 같습니다.
그래서 저의 적성과 맞는 것 같아요.
저는 이러한 부분에서 백엔드 개발자가 제가 즐길 수 있는 부분이 서로 맞기 때문에 백엔드 개발자가 되고 싶습니다 😆
-
☕️[Java] 인터페이스
1️⃣ 인터페이스.
1. 인터페이스(Interface).
자바에서 인터페이스(Interface)는 메서드의 시그니처만을 정의하는 참조 타입입니다.
인터페이스는 클래스가 구현(implement) 해야 하는 동작의 설계를 제공하며, 구현하는 모든 클래스에 대해 특정 메소드들이 반드시 존재하도록 강제합니다.
이는 다형성을 지원하는 강력한 방법으로, 서로 다른 클래스들이 동일한 인터페이스를 구현함으로써 동일한 방식으로 처리될 수 있게 해 줍니다.
1.2 인터페이스의 주요 특징.
1. 메소드 선언만 포함 : 인터페이스는 메소드의 구현을 포함하지 않습니다.(자바 8 이전까지는).
메소드의 몸체는 인터페이스를 구현하는 클래스에서 제공됩니다.
2. 상수만 포함 가능 : 인터페이스는 상수만을 멤버로 가질수 있습니다.
모든 필드는 ‘public’, ‘static’, ‘final’ 로 선언됩니다.
3. 다중 구현 지원 : 한 클래스는 여러 인터페이스를 구현할 수 있으며, 이를 통해 다중 상속의 이점을 얻을 수 있습니다.
4. 디폴트 메소드와 정적 메소드 : 자바 8 이후부터는 인터페이스에 디폴트 메소드(구현을 포함하는 메소드)와 정적 메소드를 정의할 수 있게 되었습니다.
이를 통해 더 유연한 설계가 가능해졌습니다.
1.3 인터페이스 정의 예시.
public interface Vehicle {
void start();
void stop();
}
위 예제에서 ‘Vehicle’ 인터페이스는 ‘start’ 와 ‘stop’ 이라는 두 메소드를 정의합니다.
이 인터페이스를 구현하는 모든 클래스는 이 두 메소드의 구체적인 구현을 제공해야 합니다.
1.4 인터페이스 구현 예.
public class Car implements Vehicle {
@Override
public void start() {
System.out.println("Car starts.");
}
@Override
public void stop() {
System.out.println("Car stops.");
}
}
‘Car’ 클래스는 ‘Vehicle’ 인터페이스를 구현합니다.
이 클래스는 start 와 ‘stop’ 메소드를 구체적으로 구현해야 합니다.
1.5 결론.
인터페이스는 클래스와 다른 클래스 사이의 계약을 정의하고, 특정 작업을 수행하는 메소드의 시그니처를 강제합니다.
이는 코드의 상호 운용성을 높이고, 다형성을 통한 유연한 프로그래밍 설계를 가능하게 합니다.
인터페이스를 사용함으로써 다양한 구현체를 동일한 방식으로 처리할 수 있어, 코드의 유지보수성과 확장성이 향상됩니다.
2. 상수(constant).
자바 프로그래밍에서 상수(constant)는 값이 선언 후 변경될 수 없는 변수를 의미합니다.
상수는 일반적으로 프로그램 전체에서 변하지 않는 값에 사용되며, 이는 코드의 읽기 쉬움과 유지 관리를 돕습니다.
자바에서 상수를 선언하기 위해 ‘final’ 키워드를 변수 선언과 함께 사용합니다.
2.1 상수의 특징.
1. 불변성 : 상수는 한 번 초기화되면 그 값이 변경될 수 없습니다.
2. 명확성 : 코드 내에서 직접적인 값보다는 의미 있는 이름을 가진 상수를 사용함으로써 코드의 가독성과 유지보수성이 향상됩니다.
3. 공용 사용 : 자주 사용되는 값이나 의미가 명확한 수치를 상수로 선언하여 코드 전바에 걸쳐 재사용할 수 있습니다.
2.2 상수 선언 예시.
상수를 선언하는 방법은 간단합니다.
‘final’ 키워드를 사용하여 변수를 선언하고, 초기화합니다.
일반적으로 상수의 이름은 대문자로 표기하며, 단어 간에는 언더스코어(‘_‘)를 사용합니다.
이는 상수임을 쉽게 식별할 수 있도록 도와줍니다.
public class Constants {
public static final int MAX_WIDTH = 800;
public static final int MAX_HEIGHT = 600;
public static final String COMPANY_NAME = "MyCompany";
}
위 예에서 ‘MAX_WIDTH’, ‘MAX_HEIGHT’, ‘COMPANY_NAME’ 은 모두 상수이며, 이들의 값은 선언된 후 변경될 수 없습니다.
2.3 상수 사용의 이점.
오류 감소 : 값이 한 번 설정되면 변경되지 않기 때문에, 예상치 못한 곳에서 값이 변경되어 발생할 수 있는 버그를 줄일 수 있습니다.
코드 재사용성 : 한 곳에서 값을 변경하면, 해당 상수를 사용하는 모든 위치에서 변경된 값이 적용됩니다. 이는 일관성 유지와 함께 코드 관리를 간소화합니다.
컴파일 시간 최적화 : 상수 값은 컴파일 시간에 결정되므로, 런타임에 추가적인 계산 비용이 들지 않습니다.
2.4 결론.
상수는 프로그램 내에서 변하지 않는 값을 나타내며, 코드의 안정성과 유지보수성을 높이는 데 중요한 역할을 합니다.
자바에서는 ‘final’ 키워드를 사용하여 이러한 상수를 쉽게 생성할 수 있습니다.
3. 클래스의 상속과 인터페이스의 구현을 동시에 사용.
자바에서는 클래스의 상속과 인터페이스의 구현을 동시에 사용하여 “다중 상속“과 유사한 효과를 얻을 수 있습니다.
이는 자바의 설계에서 클래스는 단일 상속만을 허용하지만, 인터페이스는 다중으로 구현할 수 있게 함으로써 이루어집니다.
3.1 단일 상속과 다중 인터페이스 구현.
단일 상속 : 자바에서 클래스는 단 하나의 상위 클래스만 상속받을 수 있습니다.
이는 C++ 같은 언어에서 볼 수 있는 다중 상속의 복잡성과 관련된 문제(예: 다이아몬드 문제)를 피하기 위함입니다.
다중 인터페이스 구현 : 한 클래스는 여러 인터페이스를 구현할 수 있습니다.
이는 인터페이스가 구체적인 구현을 포함하지 않기 때문에(자바 8 이전까지, 자바 8 이후에는 디폴트 메소드를 통해 일부 구현을 포함할 수 있음), 클래스가 여러 인터페이스를 구현함으로써 다중 상속의 효과를 나타낼 수 있습니다.
3.2 예시.
다음 예시에서 ‘Car’ 클래스는 ‘Vehicle’ 클래스를 상속받고, ‘Electric’ 및 ‘Autonomous’ 두 인터페이스를 구현하고 있습니다.
이를 통해 ‘Car’ 클래스는 ‘Vehicle’ 클래스의 속성과 메소드를 상속받으며, 동시에 두 인터페이스의 메소드를 구현해야 합니다.
class Vehicle {
void drive() {
System.out.println("This vehicle is driving.");
}
}
interface Electric {
void charge();
}
interface Autonomous {
void navigate();
}
class Car extends Vehicle implements Electric, Autonomous {
@Override
public void charge() {
System.out.println("The car is charging.");
}
@Override
public void navigate() {
System.out.println("The car is navigating autonomously.");
}
}
public class Main {
public static void main(String[] args) {
Car myCar = new Car();
myCar.drive();
myCar.charge();
myCar.navigate();
}
}
3.4 결론.
자바에서는 한 클래스가 단일 상속을 통해 한 클래스의 기능을 상속받고, 동시에 여러 인터페이스를 구현함으로써 다중 상속의 효과를 얻을 수 있습니다.
이는 자바의 타입 시스템이 제공하는 유연성을 활용하는 좋은 예시로, 소프트웨어 설계에서 필요한 다양한 기능을 조합할 수 있게 해 줍니다.
-
-
☕️[Java] 내부 클래스
1️⃣ 내부 클래스.
내부 클래스의 개념과 종류 이해
익명 클래스 직접 구현
1. 내부 클래스(Inner Class).
자바 프로그래밍에서 내부 클래스(Inner Class)는 다른 클래스의 내부에 정의된 클래스를 말합니다.
내부 클래스는 주로 외부 클래스와 밀접한 관련이 있으며, 외부 클래스의 멤버들에 대한 접근을 용이하게 하기 위해 사용됩니다.
1.1 내부 클래스의 특징.
자바의 내부 클래스에는 몇 가지 특징이 있습니다.
이 특징들은 내부 클래스가 어떻게 사용되고, 그들이 주는 이점과 한계를 이해하는 데 도움이 됩니다.
1. 접근성과 밀접성 : 내부 클래스는 외부 클래스의 모든 필드와 메소드(프라이빗 포함)에 접근할 수 있습니다. 이는 내부 클래스가 외부 클래스와 밀접한 작업을 수행할 때 매우 유용합니다.
이러한 접근은 내부 클래스가 외부 클래스의 구현 세부사항에 깊이 연결될 수 있게 합니다.
2. 캠슐화 증가 : 내부 클래스를 사용하면 관련 있는 부분만을 그룹화하여 외부에 불필요한 정보를 노출하지 않고도 복잡한 코드를 더 잘 구조화할 수 있습니다.
이는 코드의 유지보수성과 가독성을 높이는 데 도움이 됩니다.
3. 코드의 응집성 : 내부 클래스는 특정 외부 클래스와 매우 강하게 연결되어 있기 때문에, 그 기능이 외부 클래스와 밀접하게 관련된 기능을 수행할 때 코드의 응집력을 높일 수 있습니다.
4. 더 나은 논리적 그룹핑 : 특정 기능을 내부 클래스에 구현함으로써, 관련 기능과 데이터를 함께 논리적으로 그룹화할 수 있습니다.
이는 전체 코드베이스를 통해 일관성을 유지하고, 기능별로 코드를 정리하는 데 도움이 됩니다.
5. 명시적인 컨텍스트 연결 : 내부 클래스는 명시적으로 그들의 외부 클래스의 인스턴스와 연결됩니다. 이는 그들이 외부 클래스의 상태와 행동에 따라 다르게 작동할 수 있음을 의미합니다.
6. 다중 상속의 일종의 구현 : 자바는 다중 상속을 지원하지 않지만, 내부 클래스를 통해 비슷한 효과를 낼 수 있습니다. 외부 클래스가 하나 이상의 내부 클래스를 가질 수 있고, 각 내부 클래스는 다른 클래스를 상속받을 수 있으므로 다양한 기능을 조합할 수 있습니다.
7. 메모리 및 성능 고려사항 : 내부 클래스는 외부 클래스의 인스턴스와 연결되어 있기 때문에, 외부 클래스의 인스턴스가 메모리에 남아 있는 동안에는 가비지 컬렉션에서 제거되지 않습니다. 이는 메모리 관리 측면에서 고려해야 할 사항입니다.
1.2 내부 클래스의 네 가지 유형.
1. 비정적 중첩 클래스(Non-static Nested Class) 또는 내부 클래스(Inner Class) : 이 클래스는 외부 클래스의 인스턴스 멤버처럼 동작하며, 외부 클래스의 인스턴스에 대한 참조를 가지고 있습니다. 외부 클래스의 인스턴스 멤버와 메소드에 접근할 수 있습니다.
2. 정적 중첩 클래스(Static Nested Class) : 이 클래스는 외부 클래스의 정적 멤버처럼 동작하며, 외부 클래스의 인스턴스 멤버에는 접근할 수 없지만, 정적 멤버에는 접근할 수 있습니다.
3. 지역 클래스(Local Class) : 특정 메소드 또는 초기화 블록 내부에 정의된 클래스로, 선언된 영역 내에서만 사용할 수 있습니다. 지역 클래스는 해당 메소드 내에서만 사용되므로, 외부로 노출되지 않습니다.
4. 익명 클래스(Anonymous Class) : 이름이 없는 클래스로, 일반적으로 단 한 번만 사용되며 주로 리스너(listener) 또는 작은 델리게이션 클래스로 사용됩니다. 클래스 선언과 인스턴스 생성이 동시에 이루어집니다.
-
-
☕️[Java] 다형성
1️⃣ 다형성.
1. 다형성(Polymorphism)
자바에서 말하는 다형성(Polymorphism)은 객체가 여러 형태를 취할 수 있는 능력을 말합니다.
이는 같은 이름의 메소드 호출이 객체의 타입에 따라 다은 동작을 수행할 수 있게 해 주어 코드의 유연성과 재사용성을 증가시킵니다.
자바에서는 주로 두 가지 형태의 다형성을 지원하는데, 이는 컴파일 시간 다형성과 런타임 다형성입니다.
1.2. 컴파일 시간 다형성(정적 다형성).
컴파일 시간 다형성은 주로 메소드 오버로딩을 통해 구현됩니다.
메소드 오버로딩은 동일한 메소드 이름을 가지면서 매개변수 타입, 순서, 개수가 다른 여러 메소드를 같은 클래스 내에 선언하는 것을 의미합니다.
이러한 메소드들은 컴파일 시에 그 타입에 따라 구별되어 처리됩니다.
1.3 컴파일 시간 다형성 예시.
public class Display {
public void print(int num) {
System.out.println("Printing integer: " + num);
}
public void print(String str) {
System.out.println("Printing string: " + str);
}
}
1.4 런타임 다형성(동적 다형성).
런타임 다형성은 메소드 오버라이딩을 통해 구현됩니다.
이 경우 서브클래스에서 상속받은 부모 클래스의 메소드를 재정의하여 동일한 메소드 호출이 서로 다른 클래스 객체에 대해 다른 동작을 할 수 있도록 합니다.
이는 실행 중에 결정되므로 동적 다형성이라고 합니다.
1.5 런타임 다형성 예시.
class Animal {
void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
void sound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
void sound() {
System.out.println("Cat meows");
}
}
public class TestPolymorphism {
public static void main(String[] args) {
Animal myAnimal = new Dog();
myAnimal.sound(); // 출력: Dog barks
myAnimal = new Cat();
myAnimal.sound(); // 출력: Cat meows
}
}
여기서 ‘Animal’ 클래스의 ‘sound()’ 메소드는 ‘Dog’ 와 Cat 클래스에서 오버라이딩되었습니다.
‘myAnimal’ 참조 변수는 ‘Animal’ 타입이지만, 참조하는 객체의 실제 타입에 따라 적절한 ‘sound()’ 메소드가 호출됩니다.
1.6 다형성의 장점.
유연성 : 다형성을 사용하면 프로그램을 더 유연하게 설계할 수 있습니다.
예를 들어, 다양한 지식 클래스의 객체들을 부모 클래스 타입의 컬렉션에 저장하고, 각 객체에 대해 공통된 인터페이스를 통해 작업을 수행할 수 있습니다.
코드 재사용과 유지 보수의 향상 : 공통 인터페이스를 사용함으로써 코드를 재사용하고, 새로운 클래스 타입을 추가하거나 기존 클래스를 수정할 때 유지 보수가 용이해집니다.
📝 정리.
이렇게 다형성은 객체 지향 프로그래밍의 중요한 특성 중 하나로, 프로그램의 다양한 부분에서 유용하게 활용됩니다.
2. instanceof
자바 프로그래밍에서 ‘instanceof’ 연산자는 특정 객체가 지정한 타입의 인스턴스인지를 검사하는 데 사용됩니다.
이 연산자는 객체의 타입을 확인할 때 유용하게 쓰이며, 주로 객체의 실제 타입을 판별하여 안전하게 형 변환을 하기 전이나 특정 타입에 따른 조건 분기를 실행할 때 사용됩니다.
2.1 instanceof 연산자의 사용법.
‘instanceof’ 는 구 개의 피 연산자를 비교합니다.
왼쪽 피연산자는 객체를 나타내며, 오른쪽 피연산자는 타입(클래스나 인터페이스)을 나타냅니다.
연산의 결과는 불리언 값입니다.
만약 왼쪽 피연산자가 오른쪽 피연산자가 지정하는 타입의 인스턴스면 ‘true’ 를, 그렇지 않으면 ‘false’ 를 반환합니다.
기본 구조
if (object instanceof ClassName) {
// 조건이 참일 때 실행될 코드
}
예시
class Animal {}
class Dog extends Animal {}
public class Main {
public static void main(String[] args) {
Animal animal = new Animal();
Dog dog = new Dog();
Animal animalDog = new Dog();
System.out.println(animal instanceof Animal); // true
System.out.println(dog instanceof Animal); // true
System.out.println(animalDog instanceof Animal); // true
System.out.println(animal instanceof Dog); // false
}
}
이 예시에서 ‘dog instanceof Animal’ 은 ‘true’ 를 반환합니다.
왜냐하면 ‘Dog’ 클래스가 ‘Animal’ 클래스의 서브클래스이기 때문입니다.
하지만 ‘animal instanceof Dog’ 은 ‘false’ 를 반환하는데, 이는 ‘Animal’ 인스턴스가 ‘Dog’ 타입이 아니기 때문입니다.
2.2 instanceof의 주의점
1. null 검사 : ‘instanceof’ 는 객체 참조가 ‘null’ 일 때 항상 ‘false’ 를 반환합니다.
따라서 ‘null’ 값에 대한 추가적인 검사 없이도 안전하게 사용할 수 있습니다.
2. 다운캐스팅 검증 : 객체를 하위 클래스 타입으로 다운캐스팅하기 전에 ‘instanceof’ 를 사용하여 해당 객체가 실제로 해당 하위 클래스의 인스턴스인지를 확인하는 것이 안전합니다.
이를 통해 ‘ClassCastException’ 을 예발할 수 있습니다.
3. 인터페이스 검사 : ‘instanceof’ 는 클래스 뿐만 아니라 인터페이스 타입에 대해서도 사용할 수 있습니다. 객체가 특정 인터페이스를 구현하는지 여부를 검사할 수 있습니다.
📝 정리.
‘instanceof’ 는 다형성을 사용하는 객체 지향 프로그램에서 객체의 타입을 안전하게 확인하고, 타입에 맞는 적절한 동작을 수행하도록 도와주는 중요한 도구입니다.
3. 업캐스팅(Upcasting).
자바 프로그래밍에서 업캐스팅(Upcasting)은 서브클래스의 객체를 슈퍼클래스 타입의 참조로 변환하는 과정을 말합니다.
이는 일반적으로 자동으로 수행되며, 명시적으로 타입을 지정할 필요가 없습니다.
업캐스팅은 객체 지향 프로그래밍의 다형성을 활용하는 데 핵심적인 역할을 합니다.
3.1 업캐스팅의 특징과 이점.
1. 자동 형 변환 : 자바에서는 서브클래스의 객체를 슈퍼클래스 타입의 탐조 변수에 할당할 때 자동으로 업캐스팅이 발생합니다.
2. 안전성 : 업캐스팅은 항상 안전하며, 데이터 손실이나 오류 없이 수행됩니다. 이는 서브클래스가 슈퍼클래스의 모든 특성을 상속받기 때문입니다.
3. 다형적 행동 : 업캐스팅을 통해 서브클래스의 객체들을 슈퍼클래스 타입으로 다룰 수 있어, 다양한 타입의 객체들을 일관된 방식으로 처리할 수 있습니다. 이를 통해 코드의 유연성과 재사용성이 향상됩니다.
3.2 예시.
아래는 업캐스팅을 사용한 자바 코드 예시입니다.
class Animal {
public void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
public void bark() {
System.out.println("Dog is barking");
}
}
public class Main {
public static void main(String[] args) {
Dog myDog = new Dog();
Animal myAnimal = myDog; // Dog 객체를 Animal 타입으로 업캐스팅
myAnimal.eat(); // 호출 가능
// myAnimal.bark(); // 컴파일 에러, Animal 타입은 bark 메소드를 알지 못함
}
}
이 예시에서 ‘Dog’ 객체가 ‘Animal’ 타입으로 업캐스팅 되었습니다.
‘myAnimal’ 변수는 ‘Animal’ 클래스의 메소드만 호출할 수 있으며, ‘Dog’ 클래스의 ‘bark()’ 메소드는 호출할 수 없습니다.
3.3 업캐스팅 후의 제한사항.
업캐스팅을 한 후에는 원래 서브클래스의 특정 메소드나 속성에 접근할 수 없게 됩니다.
즉, 업캐스팅된 객체는 슈퍼클래스의 필드와 메소드만 사용할 수 있으며, 추가된 서브클래스의 특성은 사용할 수 없습니다.
이는 다형성의 한 예로서, 슈퍼 클래스 타입을 통해 다양한 서브클래스의 객체들을 통합적으로 다룰 수 있도록 해주며, 프로그램을 더 유연하고 확장 가능하게 만듭니다.
4. 다운캐스팅(Downcasting).
자바 프로그래밍에서 다운캐스팅(Downcasting)은 슈퍼클래스 타입의 객체 참조를 서브클래스 타입의 참조로 변환하는 과정을 말합니다.
다운캐스팅은 업캐스팅의 반대 과정으로, 업캐스팅된 객체를 다시 원래의 서브클래스 타입으로 변환할 때 사용됩니다.
다운캐스팅은 명시적으로 수행되어야 하며, 자바에서는 이 과정이 자동으로 이루어지지 않습니다.
4.1 다운캐스팅의 필요성.
업캐스팅을 통해 객체가 슈퍼클래스 타입으로 변환되면, 해당 객체는 슈퍼클래스의 메소드와 필드만 접근 가능합니다.
서브클래스에만 있는 메소드나 필드에 접근하려면 다운캐스팅을 사용하여 해당 객체를 다시 서브클래스 타입으로 변환해야 합니다.
4.2 다운캐스팅의 사용법과 주의사항.
다운캐스팅은 타입 캐스팅 연산자를 사용하여 수행되며, 반드시 ‘instanceof’ 연산자로 타입 체크를 먼저 수행하는 것이 안전합니다.
이는 변환하려는 객체가 실제로 해당 서브클래스의 인스턴스인지 확인하여 ‘ClassCastExecption’ 을 방지하기 위함입니다.
4.3 예시.
다운캐스팅을 사용하는 자바 코드 예시입니다.
class Animal {
public void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
public void bark() {
System.out.println("Dog is barking");
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Dog(); // 업캐스팅
myAnimal.eat();
// 다운캐스팅 전에 instanceof로 체크
if (myAnimal instanceof Dog) {
Dog myDog = (Dog) myAnimal; // 다운캐스팅
myDog.bark(); // 이제 서브클래스의 메소드 호출 가능
}
}
}
이 예시에서 ‘Animal’ 타입의 ‘myAnimal’ 객체는 ‘Dog’ 클래스의 인스턴스입니다.
‘myAnimal’ 을 ‘Dog’ 타입으로 다운캐스팅하여 ‘Dog’ 클래스의 ‘bark()’ 메소드에 접근할 수 있습니다.
다운캐스팅을 수행하기 전에 ‘instanceof’ 를 사용해 ‘myAnimal’ 이 실제로 ‘Dog’ 의 인스턴스인지 확인함으로써 안정을 확보합니다.
4.4 주의사항.
다운캐스팅은 객체의 실제 타입이 캐스팅하려는 클래스 타입과 일치할 때만 안전하게 수행됩니다.
잘못된 다운캐스팅은 런타임에 ‘ClassCastException’ 을 발생시킬 수 있습니다.
📝 정리.
다운캐스팅은 특정 상황에서 필수적이며, 객체의 모든 기능을 활용하기 위해 사용되지만, 항상 타입 검사를 수행하고 신중하게 사용해야 합니다.
-
☕️[Java] 추상클래스
1️⃣ 추상클래스.
추상 클래스가 무엇인지 설명할 수 있음
abstract를 이용하여 추상 클래스 구현
1. 추상 메소드(Abstract Method)
자바 프로그래밍에서 추상 메소드(abstract method)는 선언만 있고 구현은 없는 메소드입니다.
이러한 메소드는 추상 클래스(abstract class)나 인터페이스(interface) 내부에서 선언될 수 있으며, 구체적인 행동은 하위 클래스에서 구현됩니다.
추상 메소드를 사용하는 주된 목적은 하위 클래스가 특정 메소드를 반드시 구현하도록 강제하는 것입니다.
이는 코드의 일관성을 유지하고, 다형성을 통한 유연한 프로그래밍 설계를 가능하게 합니다.
1.2 추상 메소드의 특징.
선언만 있고 구현이 없음 : 메소드 본체가 없으며, 메소드 선언은 세미콜론(’;’) 으로 끝납니다.
하위 클래스에서의 구현 필수 : 추상 메소드를 포함하는 클래스를 상속받는 모든 하위 클래스는 해당 메소드를 구현해야만 인스턴스 생성이 가능합니다.
‘abstract’ 키워드 사용 : 메소드 앞에 ‘abstract’ 키워드를 명시하여 추상 메소드임을 표시합니다.
1.3 추상 메소드 예시
다음은 추상 클래스와 추상 메소드의 간단한 예시입니다.
abstract class Animal {
// 추상 메소드
abstract void makeSound();
void breathe() {
System.out.println("Btrathing...");
}
}
class Dog extends Animal {
// 추상 메소드 구현
void makeSound() {
System.out.println("Bark!");
}
}
class Cat extends Animal {
// 추상 메소드 구현
void makeSound() {
System.out.println("Meow!");
}
}
위 예에서 ‘Animal’ 클래스는 ‘makeSound’ 라는 추상 메소드를 포함하고 있습니다.
‘Dog’ 와 ‘Cat’ 클래스는 ‘Animal’ 클래스를 상속받고 ‘makeSound’ 메소드를 각각 다르게 구현하고 있습니다.
이는 다형성의 좋은 예로, ‘Animal’ 타입의 참조를 사용하여 각각의 하위 클래스 객체를 다룰 때 동일한 메소드(‘makeSound’)를 호출하더라도 서로 다른 행동(개는 짖고, 고양이는 울음)을 보여줍니다.
1.4 결론.
추상 메소드는 프로그램의 확장성과 유지보수성을 향상시키는 객체 지향 설계의 핵심 요소입니다.
다양한 상황에 맞춰 동일한 인터페이스에 여러 구현을 제공할 수 있어 유연한 코드 작성이 가능합니다.
2. 추상 클래스(abstract class)
자바에서 추상 클래스(abstract class)는 완전하지 않은 클래스로, 추상 클래스 자체로는 인스턴스를 생성할 수 없습니다.
추상 클래스의 주요 목적은 다른 클래스들의 기본이 되는 클래스를 제공하여 코드의 재사용성을 높이고, 일관된 설계를 유도하는 것입니다.
추상 클래스는 하나 이상의 추상 메소드를 포함할 수 있으며, 또한 구현된 메소드도 포함할 수 있습니다.
2.1 추상 클래스의 특징.
1. 인스턴스 생성 불가 : 추상 클래스는 직접적으로 인스턴스를 생성할 수 없습니다. 반드시 상속을 통해 그 기능을 확장하고 구체적인 클래스에서 인스턴스를 생성해야 합니다.
2. 추상 메소드 포함 가능 : 추상 클래스는 하나 이상의 메소드를 포함할 수 있습니다. 추상 메소드는 선언만 있고 구현은 없으며, 이를 상속받은 구체적인 클래스에서 구현해야 합니다.
3. 구현된 메소드 포함 가능 : 추상 클래스는 구현된 메소드도 포함할 수 있어, 자식 클래스들이 이 메소드를 재사용하거나 오버라이드 할 수 있습니다.
4. 생성자 및 필드 포함 가능 : 추상 클래스는 자신의 생성자와 필드(변수)를 가질 수 있으며, 이는 상속받은 클래스에서 사용할 수 있습니다.
2.2 추상 클래스의 사용 예시.
abstract class Animal {
abstract void makeSound();
void eat() {
System.out.println("This animal is eating.");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Bark!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Meow!");
}
}
이 예에서 ‘Animal’ 은 추상 클래스로, ‘makeSound()’ 메소드를 추상 메소드로 포함하고 있습니다.
‘Dog’ 와 ‘Cat’ 은 ‘Animal’ 클래스를 상속받아 ‘makeSound()’ 메소드를 각각 구현합니다.
추상 클래스 ‘Animal’ 의 ‘eat()’ 메소드는 모든 동물이 공통적으로 사용할 수 있는 구현된 메소드입니다.
2.3 결론.
추상 클래스는 공통적인 특징을 가진 클래스들 사이의 일반적인 행동을 정의하고, 이를 상속받는 구체적인 클래스들이 이를 구현하도록 하는 데에 주로 사용됩니다.
이를 통해 코드의 재사용성과 유지보수성을 향상시키며, 객체 지향 설계의 일관성과 안정성을 보장할 수 있습니다.
3. 익명 클래스(anonymous class).
자바에서 익명 클래스(anonymous class)는 이름이 없는 클래스입니다.
이들은 주로 일회성 사용 목적으로 설계되며, 인터페이스나 추상 클래스를 간편하게 구현하거나, 기존 클래스를 임시로 확장하기 위해 사용됩니다.
익명 클래스는 일반적으로 이벤트 리스너나 작은 콜백 객체 같이 간단한 기능을 수행하는 데에 활용됩니다.
3.1 익명 클래스의 특징.
1. 이름이 없음 : 익명 클래스는 이름을 가지지 않습니다. 인스턴스 생성 시점에 정의됩니다.
2. 즉석에서 정의 및 사용 : 익명 클래스는 즉석에서 정의되어 바로 인스턴스가 생성됩니다. 보통 이들은 한 번만 사용되고 재사용되지 않습니다.
3. 상속 및 구현 : 익명 클래스는 상쉬 클래스를 상속하거나 인터페이스를 구현할 수 있습니다. 그러나 다중 상속은 지원하지 않습니다.
4. 오직 하나의 인스턴스만 생성 가능 : 익명 클래스로부터 직접적으로 두 개 이상의 객체를 생성할 수는 없습니다. 다시 사용하려면 클래스 정의를 반복해야 합니다.
5. 지역 클래스 비슷 : 지역 변수처럼 동작하여 주변 스코프의 변수를 참조할 수 있습니다. 자바 8 이전에는 final 변수만 참조 가능했으나, 자바 8부터는 effectively final(명시적으로 final로 선언되지 않았어도 값이 변경되지 않는 변수) 변수도 참조할 수 있습니다.
3.2 익명 클래스의 사용 예.
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("버튼이 클릭되었습니다!");
}
});
위 예제에서 ‘ActionListener’ 인터페이스는 익명 클래스를 통해 구현되었습니다.
‘button.addActionListener’ 메소드에 직접 전달되면서 버튼 클릭 시 “버튼이 클릭되었습니다!”를 출력하는 ‘actionPerformed’ 메소드를 포함하고 있습니다.
3.3 결론.
익명 클래스는 특정 인터페이스나 상위 클래스의 메소드를 구현하거나 오버라이드할 때 사용됩니다.
짧고 간단한 기능을 구현하는 데 유용하며, 코드의 간결성을 유지할 수 있게 도와줍니다.
하지만 복잡한 로직이나 반복적으로 사용될 기능에 대해서는 일반 클래스나 지역 클래스를 사용하는 것이 더 적합할 수 있습니다.
-
-
☕️[Java] 상속
1️⃣ 상속.
1. 상속(Inheritance)
자바 프로그래밍에서의 상속(Inheritance)은 한 클래스가 다른 클래스의 속성과 메소드를 물려받는 기능을 말합니다.
상속을 사용하면 기존 코드를 재사용하고 확장하는 것이 용이해져, 소프트웨어의 설계와 유지 보수가 효율적으로 이루어질 수 있습니다.
1.2 상속의 주요 개념.
1. 슈퍼클래스(부모 클래스) : 기능이 상속되는 클래스입니다.
예를 들어, ‘Vehicle’ 클래스가 있을 때 클래스의 속성(예: 속도)과 메소드(예: start, stop)를 다른 클래스가 상속받을 수 있습니다.
2. 서브클래스(자식 클래스) : 슈퍼클래스의 속성과 메소드를 상속받는 클래스입니다.
서브클래스는 메소드를 그대로 사용할 수도 있고, 필요에 따라 재정의(오버라이드)할 수도 있습니다.
예를 들어, ‘Car’ 클래스가 ‘Vehicle’ 클래스를 상속받는 경우, ‘Car’ 는 ‘Vehicle’ 의 모든 속성과 메소드를 사용할 수 있으며 추가적인 기능(예: 4륜 구동 기능)을 더할 수 있습니다.
3. 메소드 오버라이딩(Method Overriding) : 서브클래스가 슈퍼클래스에서 상속받은 메소드를 재정의하여 사용하는 것 입니다.
서브클래스는 상속받은 메소드를 자신의 필요에 맞게 변경할 수 있습니다.
4. 생성자 상속 : 자바에서 생성자는 상속되지 않습니다. 서브클래스의 생성자가 호출될 때, 슈퍼클래스의 생성자도 자동으로 호출되어야 하는데, 이는 ‘super()’ 키워드를 통해 명시적으로 호출해야 합니다.
📝 정리.
상속을 사용하면 코드의 중복을 줄이고, 각 클래스의 기능을 명확하게 구분지어 설계할 수 있어 프로그램 전체의 구조가 개선됩니다.
class 자식 클래스명 extends 부모 클래스명 { // 다중 상속 불가능
필드;
메소드;
...
}
2. 상속과 접근제어자와의 관계.
자바에서 상속과 접근 제어자(Access modifiers)는 클래스와 클래스 멤버(필드, 메소드)의 접근성을 결정하는 데 중요한 역할을 합니다.
접근 제어자는 클래스의 데이터를 보호하고, 코드의 유지 보수를 용이하게 하며, 외부로부터의 불필요한 접근을 막는 기능을 합니다.
상속에서 접근 제어자는 어떤 멤버가 서브클래스에게 상속될 수 있는지, 그리고 상속받은 멤버를 서브클래스가 어떻게 활용할 수 있는지 결정짓는 요소입니다.
2.1 주요 네 가지 접근 제어자.
1. private : 멤버가 선언된 클래스 내에서만 접근 가능합니다.
‘private’ 접근 제어자가 지정된 멤버는 상속되지 않습니다.
2. default(package-private) : 접근 제어자를 명시하지 않으면, 기본적으로 ‘default’ 접근이 적용됩니다.
이러한 멤버들은 같은 패키지 내의 다른 클래스에서 접근할 수 있지만, 다른 패키지의 서브클래스에서는 접근할 수 없습니다.
3. protected : ‘protected’ 멤버는 같은 패키지 내의 모든 클래스와 다른 패키지의 서브클래스에서 접근할 수 있습니다.
이 접근 제어자는 상속을 사용할 때 특히 유용하며, 서브클래스가 슈퍼클래스의 멤버를 활용하거나 수정할 수 있게 합니다.
4, public : ‘public’ 멤버는 모든 클래스에서 접근할 수 있습니다.
상속과 관련하여, ‘public’ 멤버는 서브클래스에 의해 자유롭게 상속되고 사용될 수 있습니다.
📝 정리.
상속과 접근 제어자의 관계에서 중요한 점은, 서브클래스가 상속받은 멤버에 접근할 수 있는 권한은 슈퍼클래스에서 해당 멤버에 지정된 접근 제어자에 의해 결정된다는 것 입니다.
예를 들어, 슈퍼클래스에서 ‘protected’ 로 선언된 메소드는 서브클래스에서 접근 가능하고 필요에 따라 오버라이딩할 수 있지만, ‘private’ 으로 선언된 메소드는 서브클래스에서 직접접으로 접근하거나 사용할 수 없습니다.
이러한 제한은 객체 지향 프로그래밍에서 캡슐화와 정보 은닉을 강화하는 데 도움을 줍니다.
3. super와 super().
자바에서 ‘super’ 키워드와 ‘super()’ 생성자 호출은 상속을 사용할 때 매우 중요한 역할을 합니다.
이들은 서브클래스가 슈퍼클래스와 상호작용할 수 있게 해 줍니다.
3.1 super와 super() 키워드의 사용 방식.
3.1.1 super 키워드.
‘super’ 키워드는 슈퍼 클래스의 필드나 메소드에 접근할 때 사용됩니다.
서브클래스에서 메소드를 어버라이드 했을 때, 슈퍼클래스의 버전을 호출하고 싶은 경우에 유용하게 사용할 수 있습니다.
이는 슈퍼클래스의 구현을 활용하면서 추가적인 기능을 서브클래스에 구현할 때 필요합니다.
예를 들어, 슈퍼클래스 ‘Vehicle’ 의 메소드 ‘start()’ 를 서브클래스 ‘Car’ 에서 오버라이드한 후, ‘Car’ 의 ‘start()’ 메소드에서 ‘super.start()’ 를 호출하면, ‘Vehicle’ 클래스의 ‘start()’ 메소드가 실행됩니다.
3.1.2 super() 생성자 호출.
‘super()’ 는 서브클래스의 생성자에서 슈퍼클래스의 생성자를 호출할 때 사용됩니다.
자바에서는 모든 클래스가 생성자를 가지며, 서브클래스의 생성자가 호출될 때 슈퍼클래스의 생성자도 자동으로 호출됩니다.
명시적으로 슈퍼클래스의 생성자를 호출하고자 할 때 ‘super()’ 를 사용합니다.
이 호출은 서브클래스의 생성자의 첫 번째 명령어로 위치해야 합니다.
슈퍼클래스의 생성자를 호출함으로써, 슈퍼 클래스의 인스턴스 변수들이 적절히 초기화될 수 있습니다.
예를 들어, 슈퍼클래스 ‘Vehicle’ 에 ‘Vehicle(int speed)’ 라는 생성자가 있고, 서브클래스 ‘Car’ 에서 이를 상속 받을 때, ‘Car’ 의 생성자에서 ‘super(100)’ 을 호출하면 ‘Vehicle’ 의 생성자가 호출죄어 ‘speed’ 변수가 ‘100’ 으로 초기화됩니다.
📝 정리.
이 두 사용법은 객체지향 프로그래밍에서 클래스의 계층을 통해 기능을 확장하고 관리하는 데 필수적입니다.
‘super’ 의 사용은 상속 관계에 있는 클래스 간의 코드를 재사용하고, 유지 관리를 쉽게 하며, 다형성을 구현하는 데 중요한 역할을 합니다.
4. 오버라이딩(Overriding)
자바 프로그래밍에서 오버라이딩(Overriding)은 서브클래스가 상속받은 슈퍼클래스의 메소드를 자신의 요구에 맞게 재정의하는 과정을 말합니다.
오버라이딩은 객체 지향 프로그래밍의 핵심 개념 중 하나로, 다형성을 가능하게 하며, 상속 받은 메소드를 서브클래스에서 새로운 방식으로 구현할 수 있도록 해줍니다.
4.1 오버라이딩 규칙.
오버라이딩을 할 때는 몇 가지 규칙을 따라야 합니다.
1. 메소드 이름과 시그니처 일치 : 오버라이딩할 메소드는 슈퍼클래스의 메소드와 동일한 이름, 매개변수 목록, 반환 타입을 가져야 합니다.
2. 접근 제어 : 오버라이딩하는 메소드는 슈퍼클래스의 메소드보다 더 제한적인 접근 제어를 가질 수 없습니다.
예를 들어, 슈퍼클래스의 메소드가 ‘public’ 이라면 서브클래스의 오버라이딩 메소드도 적어도 ‘public’ 이어야 합니다.
3. 반환 타입 : 오버라이딩하는 메소드의 반환 타입은 슈퍼클래스의 메소드 반환 타입과 같거나 그 하위 타입이어야 합니다.(이것은 공변 반환 타입이라고 함.)
4.2 오버라이딩의 예.
슈퍼클래스 ‘Animal’ 에 다음과 같은 메소드가 있다고 가정해 봅시다.
public class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
이제 ‘Dog’ 클래스가 ‘Animal’ 클래스를 상속받고 ‘makeSound()’ 메소드를 오버라이드하여 다음과 같이 구현할 수 있습니다.
public class Dog extends Animal {
@Override // 이 어노테이션은 선택적이지만, 오버라이딩임을 명시적으로 나타냅니다.
public void makeSound() {
System.out.println("Dog barks");
}
}
이 예에서 ‘Dog’ 클래스의 ‘makeSound()’ 메소드는 ‘Animal’ 의 makeSound() 메소드를 오버라이드하여 “Dog barks”를 출력하도록 재정의합니다.
4.3 오버라이딩의 중요성.
오버라이딩은 다음과 같은 이점을 제공합니다.
유연성 : 같은 메소드 호출이지만, 다양한 서브클래스에서 서로 다른 동작을 구현할 수 있습니다.
재사용성 : 기존의 코드를 변경하지 않고도, 상속받은 메소드를 새로운 요구에 맞게 확장할 수 있습니다.
유지보수 : 코드의 중복을 줄이고, 유지보수를 간편하게 할 수 있습니다.
📝 정리.
오버라이딩은 프로그램의 다형성을 구현하는 데 필수적인 기능으로, 상속받은 메소드를 사용하는 대신 서브클래스에 맞게 특화된 기능을 구현할 수 있도록 합니다.
-
-
☕️[Java] 클래스와 객체(2)
1️⃣ 클래스와 객체(2)
1. 오버로딩(Overloading).
자바 프로그래밍에서 오버로딩(Overloading)은 같은 클래스 내에서 메소드 이름이 같지만 매개변수의 타입이나 개수가 다른 여러 메소드를 정의하는 것을 의미합니다.
오버로딩을 사용하면 같은 기능을 하는 메소드라도 다양한 입력에 대응하여 유연하게 메소드를 호출할 수 있습니다.
오버로딩은 메소드만 가능하며, 생성자에도 적용될 수 있습니다.
1.2. 오버로딩의 규칙.
1. 메소드 이름이 같아야 합니다 : 오버로딩된 메소드들은 같은 이름을 공유합니다.
2. 매개변수 목록이 달라야 합니다 : 매개변수의 개수나 타입, 혹은 그 순서가 달라야 합니다. 매개변수의 차이를 통해 자바 컴파일러는 호출할 적절한 메소드를 결정합니다.
3. 반환 타입은 오버로딩을 구분하는 데 사용되지 않습니다 : 오버로딩된 메소드는 반환 타입이 다르더라도, 이는 오버로딩을 구분하는 데 사용되지 않습니다.
즉, 반환 타입만 다른 메소드는 오버로딩이 아닙니다.
4. 접근 제어자와 예외는 오버로딩을 구분하는 데 사용되지 않습니다 : 이 역시 메소드를 구별하는 데 사용되지 않습니다.
1.3. 오버로딩의 예.
public class Print {
// 오버로딩 예제: 같은 메소드 이름, 다른 매개변수 타입
public void display(int a) {
System.out.println("Integer: " + a);
}
public void display(String a) {
System.out.println("String: " + a);
}
// 오버로딩 예제: 같은 메소드 이름, 다른 매개변수 개수
public void display(int a, int b) {
System.out.println("Two integers: " + a + ", " + b);
}
// 오버로딩 예제: 같은 메소드 이름, 매개변수의 순서가 다름
public void display(String a, int b) {
System.out.println("String and integer: " + a + ", " + b);
}
}
public class Test {
public static void main(String[] args) {
Print prt = new Print();
prt.display(1); // 출력: Integer: 1
prt.display("Hello"); // 출력: String: Hello
prt.display(1, 2); // 출력: Two integers: 1, 2
prt.display("Age", 30); // 출력: String and integer: Age, 30
}
}
이 예제에서 ‘Print’ 클래스는 ‘display’ 라는 메소드를 여러 번 오버로딩했습니다.
매개변수의 타입, 개수, 순서에 따라 다른 메소드가 호출됩니다.
이를 통해 다양한 타입과 개수의 입력을 유연하게 처리할 수 있습니다.
📝 정리.
오버로딩을 통해 프로그램의 가독성을 향상시키고, 유사한 기능을 하는 메소드들을 하나의 이름으로 그룹화함으로써 프로그램을 더욱 직관적으로 만들 수 있습니다.
이러한 방식은 프로그래밍의 복잡성을 줄이고, 코드의 유지보수를 용이하게 합니다.
2. 접근제어자(Access Modifiers).
자바 프로그래밍에서 접근 재어자(Access Modifiers)는 클래스, 메서드, 변수 등과 같은 멤버들에 대한 접근 권한을 제어하는 키워드입니다.
이러한 접근 제어자를 사용함으로써 클래스의 캡슐화를 강화할 수 있으며, 객체의 데이터와 메서드를 외부에서 직접접으로 접근하거나 수정하는 것을 제한할 수 있습니다.
접근 제어자는 클래스의 멤버(변수, 메서드, 생성자 등)와 클래스 자체에 적용될 수 있습니다.
2.1 자바에서 사용하는 주요 접근 제어자
1. public : 어떤 클래스에서든 접근할 수 있도록 허용합니다.
public으로 선언됩 멤버는 어디서든 접근이 가능합니다.
2. protected : 같은 패키지 내의 클래스 또는 다른 패키지의 서브 클래스에서 접근할 수 있습니다.
3. default(package-private) : 접근 제어자를 명시하지 않은 경우, 같은 패키지 냐의 클래스들만 접근할 수 있습니다. 이를 종종 package-private라고도 합니다.
private : 해당 멤보를 선언한 클래스 내에서만 접근할 수 있습니다. 외부 클래스에서는 접근할 수 없어, 클래스 내부 구현을 숨기는 데 유용합니다.
2.2 접근 제어자의 사용 예제.
public class AccessExample {
public int publicVar = 10; // 어디서든 접근 가능
protected int protectedVar = 20; // 같은 패키지 또는 상속 받은 클래스에서 접근 가능
int defaultVar = 30; // 같은 패키지 내에서만 접근 가능
private int privateVar = 40; // 이 클래스 내에서만 접근 가능
public void show() {
System.out.println("publicVar: " + publicVar);
System.out.println("protectedVar: " + pretectedVar);
System.out.println("defaultVar: " + defaultVar);
System.out.println("privateVar: " + privateVar);
}
}
public class Test {
public static void main(String[] args) {
AccessExample example = new AccessExample();
System.out.println(example.publicVar); // 접근 가능
System.out.println(example.protectedVar); // 다른 패키지에 있지 않은 이상 접근 가능
System.out.println(example.defaultVar); // 같은 패키지에 있을 경우 접근 가능
// System.out.println(example.privateVar); // 컴파일 에러 발생, 접근 불가능
example.show(); // 모든 변수 출력 가능
}
}
위 예제에서는 다양한 접근 제어자가 적용된 변수들을 선언하고, 이에 대한 접근 가능성을 보여줍니다.
‘publicVar’ 은 어디서든 접근할 수 있지만, ‘privateVar’ 는 오직 선언된 클래스 내부에서만 접근할 수 있습니다.
‘protectedVar’ 과 ‘defaultVar’ 는 좀 더 제한적인 접근을 허용합니다.
📝 정리.
이렇게 접근 제어자를 통해 자바에서는 데이터 보호 및 캡슐화, 객체의 정확한 사용을 보장하여 프로그램의 안정성과 유지보수성을 향상시킬 수 있습니다.
3. static 키워드.
자바 프로그래밍에서 ‘static’ 키워드는 클래스의 멤버(필드, 메서드, 블록 또는 내부 클래스)를 클래스 레벨에 소속 시키는 역할을 합니다.
이는 특정 인스턴스에 속하기보다는 클래스 자체에 속한다는 의미입니다.
‘static’ 멤버는 클래스의 모든 인스턴스에 의해 공유되며, 클래스가 메모리에 로드될 때 생성되고, 클래스가 언로드될 때 소멸됩니다.
3.1 static의 특징.
1. 클래스 레벨에서 공유 : ‘static’ 필드는 클래스의 모든 인스턴스 간에 공유됩니다.
이는 특정 데이터를 모든 객체가 공유해야 할 필요가 있을 때 유용합니다.
2. 인스턴스 생성 없이 접근 기는 : ‘static’ 메서드나 필드는 객체의 인스턴스를 생성하지 않고도 클래스 이름을 통해 직접 접근할 수 있습니다.
3. 정적 초기화 블록 : ‘static’ 키워드를 사용한 블록(정적 블록)은 클래스가 처음 메모리에 로그 될 때 단 한 번 실행됩니다.
이는 ‘static’ 필드의 초기화에 사용할 수 있습니다.
3.2 static 필드와 메서드 사용 예.
public class Calculator {
// 정적 필드
public static int calculatorCount = 0;
// 정적 블록
static {
System.out.println("Calculator 클래스 로딩!");
}
// 생성자
public Calculator() {
calculatorCount++; // 생성될 때마다 계산기의 수를 증가
}
// 정적 메서드
public static int add(int a, int b) {
return a + b;
}
}
public class Test {
public static void main(String[] args) {
Calculator c1 = new Calculator();
Calculator c2 = new Calculator();
System.out.println("Created Calculators: " + Calculator.calculatorCount); // 2 출력
System.out.println("Sum: " + Calculator.add(5,3)); // 8 출력
}
}
이 예제에서는 ‘Calculator’ 클래스의 인스턴스 생성 횟수를 추적하는 ‘static’ 필드 ‘calculatorCount’ 와 정적 메서드 ‘add’ 를 사용합니다.
‘calculatorCount’ 는 ‘Calculator’ 의 모든 인스턴스에 의해 공유되며, ‘add’ 메서드는 인스턴스를 생성하지 않고도 호출할 수 있습니다.
3.3 static 사용 시 주의점
‘static’ 메서드 내에서는 인스턴스 변수나 메서드를 직접 사용할 수 없습니다.
‘static’ 은 남용하면 객체지향의 원칙을 해칠 수 있습니다.
예를 들어, 객체 간의 상태 공유가 과도하게 이루어져 객체 간의 결합도가 높아질 수 있습니다.
‘static’ 변수는 프로그램의 실행이 끝날 때까지 메모리에 남아 있으므로 메모리 사용에 주의해야 합니다.
3.4 static 메소드와 static 변수와의 관계성.
자바 프로그래밍에서 ‘static’ 메소드와 ‘static’ 변수는 두 가지 공통점을 가지고 있습니다.
둘 다 클래스 레벨에서 정의되며, 클래스의 모든 인스턴스 간에 공유됩니다.
이런 공통점 때문에, ‘static’ 메소드는 ‘static’ 변수에 직접 접근할 수 있지만, 일반 인스턴스 변수에는 접근할 수 없습니다.
3.5 static 변수.
‘static’ 변수는 클래스 변수라고도 하며, 특정 클래스의 모든 인스턴스에 의해 공유됩니다.
이 변수는 클래스가 메모리에 로드될 때 생성되고, 클래스가 언로드될 때까지 메모리에 존재합니다.
‘static’ 변수는 특히 클래스의 인스턴스들이 공통적으로 사용해야 하는 데이터를 저장하는데 유용합니다.
예를 들어, 모든 계산기 객체가 공유해야 하는 ‘calculatorCount’ 와 같은 경우에 사용됩니다.
3.6 static 메소드
‘static’ 메소드 역시 클래스 레벨에 정의되며, 이 메소드는 인스턴스 생성 없이 클래스 이름을 통해 직접 호출할 수 있습니다.
‘static’ 메소드는 인스턴스 필드나 메소드에 접근할 수 없습니다.
그 이유는 ‘static’ 메소드가 호출될 때 해당 클래스의 인스턴스가 존재하지 않을 수 있기 때문입니다.
따라서 ‘static’ 메소드는 오로지 ‘static’ 변수나 다른 ‘static’ 메소드에만 접근할 수 있습니다.
3.7 두 요소의 상호작용.
‘static’ 메소드에는 ‘static’ 변수에 자유롭게 접근하고 수정할 수 있습니다.
이는 ‘static’ 변수가 클래스에 속하고 메소드도 클래스 레벨에서 실행되기 때문입니다.
예를 들어, 어떤 클래스의 모든 인스턴스가 사용할 설정 값을 ‘static’ 변수에 저장하고, 이 값을 설정하거나 조회하는 ‘static’ 메소드를 제공할 수 있습니다.
3.8 예제
public class Counter {
public static int count = 0; // 'static' 변수
public static void increment() { // 'static' 메소드
count++; // 'static' 변수에 접근하여 값을 증가
}
public static void displayCount() {
System.out.println("Count: " + count); // 'static' 변수의 현재 값을 출력
}
}
public class Test {
public static void main(String[] args) {
Counter.increment();
Counter.increment();
Countet.displayCount(); // 출력: Count: 2
}
}
이 예제에서 ‘Counter’ 클래스는 ‘static’ 변수 ‘count’ 를 가지고 있으며, increment 메소드를 통해 이 변수의 값을 증가시키고, ‘displayCount’ 메소드를 통해 값을 출력합니다.
모든 ‘Counter’ 객체가 ‘count’ 값을 공유하며, ‘static’ 메소드를 통해 이 값을 조작할 수 있습니다.
📝 정리.
‘static’ 은 전역 변수나 전역 메서드와 유사한 효과를 제공하지만, 자바의 객체지향적 특성과 일관성을 유지하기 위해 적절히 사용되어야 합니다.
‘static’ 메소드와 ‘static’ 변수는 클래스 레벨에서 관리되어 클래스의 모든 인스턴스에 의해 공유되는 특성을 가지고 있습니다.
이를 통해 클래스 전체에 영향을 미치는 작업을 수행할 수 있습니다.
-
-
☕️[Java] 다차원 배열
1️⃣ 다차원 배열.
자바 프로그래밍에서 다차원 배열이란, 배열의 배열을 의미합니다.
이는 데이터를 행렬이나 그리드 형태로 구성할 수 있게 해주며, 주로 2차원 이상의 데이터 구조를 필요로 할 때 사용됩니다.
가장 흔한 형태는 2차원 배열이지만, 3차원 이상의 배열도 만들 수 있습니다.
1. 2차원 배열.
2차원 배열은 행렬과 비슷하게 생각할 수 있으며, 각 행과 열에 데이터를 저장합니다.
예를 들어, 숫자로 이루어진 표를 저장하거나 정보를 격자 형태로 관리할 때 유용합니다.
1.2 2차원 배열의 초기화
자바에서 이차원 배열을 초기화하는 방법은 크게 세 가지로 나눌 수 있습니다.
배열을 선언할 때 크기만 지정해 두거나, 선언과 동시에 특정 값을 사용하여 초기화하거나, 나중에 각 요소에 값을 할당할 수 있습니다.
아래는 각 방법에 대한 설명과 예제입니다.
1.1.1 크기만 지정하여 배열 선언하기.
이 방법은 배열의 행과 열의 크기를 지정해 초기화하지만, 배열의 각 요소는 자동으로 기본 값으로 설정됩니다.(예: int의 경우 0).
inu[][] array = new int[3][4]; // 3행 4열의 배열 생성
이렇게 선언된 배열은 모든 요소가 0으로 초기화됩니다.
1.1.2 선언과 동시에 초기값을 제공하여 배열 초기화하기.
배열을 선언하면서 동시에 초기값을 제공할 수 있습니다.
이 방법은 배열의 내용을 명확히 알고 있을 때 유용합니다.
int[][] array = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
}; // 각 행에 대한 값들을 중괄호로 묶어서 초기화
이 예제에서 배열은 3행 4열의 구조로, 각 행의 값이 명시적으로 초기화되어 있습니다.
1.1.3 반복문을 사용하여 배열 초기화하기.
반복문을 사용하면 배열의 각 요소를 동적으로 초기화할 수 있습니다.
이 방법은 런타임에 따라 배열 값을 설정해야 할 때 유용합니다.
int[][] array = new int[3][4];
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
array[i][j] = (i + 1) * (j + 1); // 각 요소를 행 인덱스와 열 인덱스의 곱으로 초기화
}
}
이 방법은 배열의 각 위치에 i와 j 인덱스에 의존하는 계산 결과를 저장합니다.
📝 정리.
이 세 가지 방법은 상황에 따라 각기 다른 이점을 제공하므로, 요구 사항에 맞게 선택하여 사용할 수 있습니다.
2. 3차원 배열.
3차원 배열은 데이터를 3차원 공간으로 구성하여 저장합니다.
이는 비디오 게임의 공간 데이터, 과학 실험 데이터 등 복잡한 정보를 구조화하는 데 사용될 수 있습니다.
2.1. 3차원 배열의 초기화.
자바에서 3차원 배열을 초기화하는 방법은 2차원 배열과 유사합니다.
크게 세 가지 방법으로 나눌 수 있습니다.
크기만 지정하여 선언하기.
선언과 동시에 구체적인 값으로 초기화하기
반복문을 사용하여 동적으로 초기화하기
아해는 각 방법에 대한 설명과 예시입니다.
2.1.1. 크기만 지정하여 배열 선언하기.
이 방법은 삼차원 배열의 각 차원의 크기를 지정합니다.
각 요소는 자동으로 기본값으로 설정됩니다.(예: ‘int’ 의 경우 0).
int[][][] array = new int[3][4][5] // 3개의 4x5 행렬을 갖는 삼차원 배열
이 배열은 3개의 2차원 배열을 가지며, 각 2차원 배열은 4행 5열 구조입니다.
2.1.2. 선언과 동시에 초기값을 제공하여 배열 초기화하기.
삼차원 배열을 선언하면서 바로 값을 지정할 수 있습니다.
이 방법은 각 요소의 초기값을 명확히 알고 있을 때 매우 유용합니다.
int [][][] array = {
{
{1, 2, 3, 4, 5},
{6, 7, 8, 9, 10},
{11, 12, 13, 14, 15},
{16, 17, 18, 19, 20}
},
{
{21, 22, 23, 24, 25},
{26, 27, 28, 29, 30},
{31, 32, 33, 34, 35},
{36, 37, 38, 39, 40}
},
{
{41, 42, 43, 44, 45},
{46, 47, 48, 49, 50},
{51, 52, 53, 54, 55},
{56, 57, 58, 59, 60}
}
}; // 각 행렬 및 행에 대한 값들을 중괄호로 묶어서 초기화
2.1.3. 반복문을 사용하여 배열 초기화하기
반복문을 사용해 삼차원 배열의 각 요소를 동적으로 초기화할 수 있습니다.
이 방법은 프로그램 실행 중에 배열 값을 설정해야 할 때 매우 유용합니다.
int[][][] array = new int[3][4][5];
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
for (int k = 0; k < array[i][j].length; k++) {
array[i][j][k] = (i + 1) * (j + 1) * (k + 1) // 각 요소를 i, j, k 인덱스의 곱으로 초기화
}
}
}
이 예제에서는 각 위치에 해당하는 인덱스의 곱을 저장하여 배열을 초기화하고 있습니다.
이러한 초기화 방법은 특히 배열의 구조가 복잡할 때 배열을 효과적으로 관리할 수 있게 도와줍니다.
-
☕️[Java] 클래스와 객체(1)
1️⃣ 클래스와 객체(1)
1. 클래스(Class)
자바 프로그래밍 언어에서 클래스는 객체를 생성하기 위한 설계도 혹은 템플릿입니다.
클래스는 객체의 상태를 정의하는 필드(변수)와 객체의 행동을 정의하는 메서드(함수)로 구성됩니다.
클래스를 사용하는 주된 목적은 데이터와 그 데이터를 조작하는 방법들은 하나의 장소에 묶어 관리하기 위함입니다.
이를 통해 데이터 추상화, 캡슐화, 상속, 다형성 등의 객체지향 프로그래밍의 주요 개념들을 구현할 수 있습니다.
1.2 클래스의 구성 요소.
1. 필드(Field) : 객체의 데이터 또는 상태를 저장하는 변수입니다.
2. 메서드(Method) : 객체가 수행할 수 있는 행동을 정의한 코드 블록입니다.
메서드는 필드의 값을 처리하거나 다른 메서드를 호출할 수 있습니다.
3. 생성자(Constructor) : 클래스로부터 객체를 생성할 때 초기화를 담당하는 특별한 종류의 메서드입니다.
생성자는 클래스 이름과 같은 이름을 가집니다.
1.3 클래스 예제
자바에서의 간단한 클래스 예제를 살펴보겠습니다.
public class Car {
// 필드(변수)
private String color;
private String model;
// 생성자
public Car(String color, String model) {
this.color = color;
this.model = model;
}
// 메서드
public void drive() {
System.out.println(model + " 색상의 " + color + " 자동차가 주행 중입니다.");
}
}
// 객체 생성 및 사용
public class Test {
public static void main(String[] args) {
Car myCar = new Car("레드", "테슬라");
myCar.drive();
}
}
위의 예제에서 ‘Car’ 클래스는 ‘color’와 ‘model’이라는 두 개의 필드를 가지며, 이는 각각 자동차의 색상과 모델을 나타냅니다.
‘Car’ 클래스의 객체를 생성할 때는 ‘new’ 키워드와 함께 생성자를 호출하여 초기 상태를 설정합니다.
‘drive’ 메서드는 자동차가 주행하고 있음을 시뮬레이션하는 기능을 합니다.
📝 정리.
클래스를 사용함으로써 코드의 재사용성, 관리성 및 확장성이 향상되며, 대규모 소프트웨어 개발에서 필수적인 요소가 됩니다.
2. 객체(Object)와 인스턴스(Instance).
자바 프로그래밍에서 “객체(Object)”와 “인스턴스(Instance)”는 매우 중요한 개념입니다.
이 두 용어는 종종 서로 바꿔 쓰이지만, 각각의 의미에는 약간의 차이가 있습니다.
2.1 객체(Object).
객체는 소프트웨어 세계의 구성 요소로, 실제 세계의 객체를 모방한 것입니다.
객체는 데이터(속성)와 그 데이터를 조작할 수 있는 함수(메서드)를 캡슐화합니다.
객체는 클래스에 정의된 속성과 기능을 실제로 사용할 수 있도록 메모리상에 할당된 구조입니다.
객체의 개념은 클래스의 특성을 실제로 구현하는 것입니다.
2.2 인스턴스(Instance).
인스턴스는 클래스 타입에 따라 생성된 객체를 의미합니다.
예를 들어, ‘Car’ 클래스의 구체적인 객체(예: 빨간색 테슬라 자동차, 파란색 현대 자동차 등)는 모두 ‘Car’ 클래스의 인스턴스입니다.
즉, 인스턴스는 특정 클래스의 구현체입니다.
인스턴스라는 용어는 주로 객체가 메모리에 할당되어 실제로 생성되었음을 강조할 때 사용됩니다.
2.3 객체와 인스턴스의 관계.
간단히 말해, 모든 인스턴스는 객체입니다, 하지만 사용된 맥락에 따라 ‘인스턴스’라는 용어는 그 객체가 특정 클래스의 구현체임을 명시적으로 나타낼 때 사용됩니다.
예를 들어, 우리가 ‘new Car(“blue”, “Hyundai”)’ 를 통해 생성한 객체는 ‘Car’ 클래스의 인스턴스입니다.
2.4 예제 코드.
public class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public void speak() {
System.out.println(name + " makes a noise.");
}
}
public class Test {
public static void main(String[] args) {
// 여기서 'dog'는 Animal 클래스의 객체이자 인스턴스입니다.
Animal dog = new Animal("Dog");
dog.speak();
}
}
위 예제에서 ‘Animal’ 클래스가 있고, ‘main’ 메서드에서 ‘Animal’ 클래스의 새 객체를 생성합니다.
여기서 ‘dog’ 는 ‘Animal’ 클래스의 인스턴스이며 객체입니다.
‘dog’ 는 ‘Animal’ 클래스에 정의된 메서드와 필드를 사용할 수 있습니다.
📝 정리.
요약하면, 객체는 속성과 메서드를 갖는 소프트웨어의 기본 구성 단위이고, 인스턴스는 그 객체가 특정 클래스의 실제 구현체임을 의미합니다.
이 두 용어는 프로그래밍에서 클래스 기반의 객체를 생성하고 다룰 때 핵심적인 역할을 합니다.
클래스와 객체의 관계를 이해
기본 사용 방법과 생성자 및 this의
3. 메소드(Method).
자바 프로그래밍에서 메소드(Method)는 클래스에 속한 함수로서, 특정 작업을 수행하는 코드 블록입니다.
메소드는 객체의 행동을 정의하며, 클래스 내에서 정의된 데이터나 상태(필드)를 조작하는 데 사용됩니다.
메소드를 통해 객체지향 프로그래밍의 중요한 특징인 캡슐화와 추상화를 구현할 수 있습니다.
3.1 메소드의 주요 특징.
1. 재사용성 : 메소드는 코드의 재사용성을 증가시킵니다. 한 번 정의된 메소드는 여러 위치에서 호출되어 사용될 수 있습니다.
2. 모듈성 : 메소드를 사용함으로써 큰 프로그램을 작은 단위로 나누어 관리할 수 있습니다. 이는 코드의 가독성과 유지보수성을 향상시킵니다.
3. 정보 은닉 : 메소드를 통해 구현 세부사항을 숨기고 사용자에게 필요한 기능만을 제공할 수 있습니다.
3.2 메소드의 구성 요소.
1. 메소드 이름 : 메소드를 식별하는 데 사용되며, 메소드가 수행하는 작업을 설명하는 명확한 이름을 가집니다.
2. 매개변수 목록(Parameter List) : 메소드에 전달되는 인자의 타입, 순서, 그리고 개수를 정의합니다. 매개변수는 선택적일 수 있습니다.
3. 반환 타입 : 메소드가 작업을 수행한 후 반환하는 데이터의 타입입니다. 반환할 데이터가 없으면 ‘void’ 로 지정됩니다.
4. 메소드 바디 : 실제로 메소드가 수행할 작업을 구현하는 코드 블록입니다.
3.3 예제.
자바에서 간단한 메소드 예제를 보여드리겠습니다.
public class Calculator {
// 메소드 정의: 두 정수의 합을 반환
public int add(int num1, int num2) {
return num1 + num2;
}
// 메소드 정의: 두 정수의 차를 반환
public int subtract(int num1, int num2) {
return num1 - num2;
}
}
public class Test {
public static void main(String[] args) {
Calculator calc = new Calculator();
int result1 = calc.add(5, 3); // 8 반환
int result2 = calc.subtract(5, 3); // 2 반환
System.out.println("Addition Result: " + result1);
System.out.println("Subtraction Result: " + result2);
}
}
이 예제에서 ‘Calculator’ 클래스는 두 개의 메소드 ‘add’ 와 ‘subtract’ 를 가지고 있습니다.
각각의 메소드는 두 개의 정수를 받아 그 결과를 반환합니다.
이렇게 메소드를 사용하면 코드를 효율적으로 관리할 수 있으며, 필요에 따라 재사용할 수 있습니다.
📝 정리.
메소드는 자바 프로그래밍에서 기능을 모듈화하고 코드의 재사용을 가능하게 하는 핵심 요소입니다.
4. 접근 제어자(Access Modifiers)
자바 프로그래밍에서 접근 제어자(Access Modifiers)는 클래스, 메서드, 변수 등과 같은 멤버들에 대한 접근 권한을 제어하는 키워드입니다.
이러한 접근 제어자를 사용함으로써 클래스의 캡슐화를 강화할 수 있으며, 객체의 데이터와 메서드를 외부에서 직접적으로 접근하거나 수정하는 것을 제한할 수 있습니다.
접근 제어자는 클래스의 멤버(변수, 메서드, 생성자 등)와 클래스 자체에 적용될 수 있습니다.
4.1 자바에서 사용하는 주요 접근 제어자.
1. public : 어떤 클래스에서든 접근할 수 있도록 허용합니다.
public으로 선언된 멤버는 어디서든 접근이 가능합니다.
2. protected : 같은 패키지 내의 클래스 또는 다른 패키지의 서브 클래스에서 접근할 수 있습니다.
3. default(package-private) : 접근 제어자를 명시하지 않은 경우, 같은 패키지 내의 클래스들만 접근할 수 있습니다.
이를 종종 package-private라고도 합니다.
4. private : 해당 멤버를 선언한 클래스 내에서만 접근할 수 있습니다.
외부 클래스에서는 접근할 수 없어, 클래스 내부 구현을 숨기는 데 유용합니다.
4.2 접근 제어자의 사용 예제.
public class AccessExample {
public int publicVar = 10; // 어디서든 접근 가능
protexted int protectedVar = 20; // 같은 패키지 또는 상속받은 클래스에서 접근 가능
int defaultVar = 30; // 같은 패키지 내에서만 접근 가능
private int privateVar = 40; // 이 클래스 내에서만 접근 가능
public void show() {
System.out.println("publicVar: " + publicVar);
System.out.println("protectedVar: " + protectedVar);
System.out.println("defaultVar: " + defaultVar);
System.out.println("privateVar: " + privateVar);
}
}
public class Test {
public static void main(String[] args) {
AccessExample example = new AccessExample();
System.out.println(example.publicVar); // 접근 가능
System.out.println(example.protectedVar); // 다른 패키지에 있지 않은 이상 접근 가능
System.out.println(example.defaultVar); // 같은 패키지에 있을 경우 접근 가능
// System.out.println(example.privateVar); // 컴파일 에러 발생, 접근 불가능
example.show(); // 모든 변수 출력 가능
}
}
위 예제에서는 다양한 접근 제어자가 적용된 변수들을 선언하고, 이에 대한 접근 가능성을 보여줍니다.
‘publicVar’ 은 어디서든 접근할 수 있지만, ‘privateVar’ 는 오직 선언된 클래스 내부에서만 접근할 수 있습니다.
‘protectedVar’ 과 ‘defaultVar’ 는 좀 더 제한적인 접근을 허용합니다.
📝 정리.
이렇게 접근 제어자를 통해 자바에서는 데이터 보호 및 캡슐화, 객체의 정확한 사용을 보장하여 프로그램의 안정성과 유지보수성을 향상시킬 수 있습니다.
5. static 키워드.
자바 프로그래밍에서 ‘static’ 키워드는 특정 필드나 메소드, 또는 중첩 클래스를 클래스의 인스턴스가 아닌 클래스 자체에 소속되게 합니다.
이를 사용함으로써 해당 멤버는 클래스의 모든 인스턴스에 걸쳐 공유되며, 인스턴스 생성 없이 클래스 이름을 통해 직접 접근할 수 있습니다.
5.1 static의 주요 사용 사례.
1. 정적 필드(Static Fields) : 모든 인스턴스에 의해 공유되는 클래스 변수입니다.
예를 들어, 회사의 모든 직원이 같은 회사 이름을 공유할 때 사용할 수 있습니다.
2. 정적 메소드(Static Methods) : 인스턴스 변수에 접근할 필요 없이, 클래스 이름을 통해 직접 호출할 수 있는 메소드입니다.
유틸리티 함수나 핼퍼 함수를 작성할 때 자주 사용됩니다.
3. 정적 초기화 블록(Static Initialization Blocks) : 클래스가 처음 로딩될 때 한 번 실행되며, 정적 변수를 초기화하는 데 사용됩니다.
4. 정적 중첩 클래스(Static Nested Classes) : 다른 클래스 내부에 위치하면서도 독립적으로 사용될 수 있는 클래스입니다.
5.2 static 키워드의 장점과 단점.
장점.
메모리 효율성 : static 멤버는 클래스 로드 시 메모리에 한 번만 할당되고 모든 인스턴스가 공유하기 때문에 메모리 사용을 최소화할 수 있습니다.
편리성 : 객체 생성 없이 바로 접근할 수 있어, 유틸리티 함수 같은 공통 기능 구현에 유용합니다.
단점.
과도한 사용은 객체지향 원칙에 어긋남 : 객체 간의 결합도가 높아지고, 객체의 상태 관리가 어려워질 수 있습니다.
테스트가 어려워질 수 있슴 : static 메소드는 오버라이드가 불가능하며, 상태를 공유하기 때문에 병렬 테스트 환경에서 문제를 일으킬 수 있습니다.
5.3 예제.
public class Company {
// 정적 필드
public static String companyName = "Global Tech";
// 정적 메소드
public static void printCompanyName() {
System.out.println("Company Name: " + companyName);
}
}
public class Test {
public static void main(String[] args) {
// 객체 생성 없이 정적 메소드 호출
Company.printCompanyName();
}
}
이 예제에서 ‘Company’ 클래스에는 정적 필드 ‘companyName’ 과 정적 메소드 ‘printCompanyName()’ 이 있습니다.
‘main’ 메소드에서는 ‘Company’ 클래스의 객체를 생성하지 않고도 ‘printCompanyName()’ 메소드를 호출하려 회사 이름을 출력합니다.
📝 정리.
정적 멤버는 클래스와 관련된, 변하지 않는 값이나, 모든 인스턴스가 공유해야 하는 정보를 관리할 때 유용하게 사용됩니다.
6. 생성자(Constructor)
자바 프로그래밍에서 생성자(Constructor)는 클래스로부터 객체가 생성될 때 호출되는 특별한 유형의 메서드입니다.
생성자의 주요 목적은 새로 생성된 객체를 초기화하는 것으로, 객체의 기본 상태를 설정하는 데 사용됩니다.
생성자는 메서드처럼 보일 수 있지만, 리턴 타입이 없고 클래스 이름과 동일한 이름을 가집니다.
6.1 생성자 특징.
1. 클래스 이름과 동일 : 생성자의 이름은 항상 선언된 클래스의 이름과 동일해야 합니다.
2. 리턴 타입 없음 : 생성자는 값을 반환하지 않으며, 리턴 타입도 선언하지 않습니다.
3. 자동 호출 : 객체가 생성될 때 자동으로 호출됩니다.
이는 객체의 필드를 초기화하거나, 객체 생성 시 실행해야 할 다른 시작 루틴을 실행하는 데 사용할 수 있습니다.
4. 오버로딩 가능 : 하나의 클래스에 여러 생성자를 정의할 수 있습니다.
이를 생성자 오버로딩이라고 하며, 파라미터의 수나 타입에 따라 다른 생성자를 호출할 수 있습니다.
6.2 생성자의 유형.
1. 기본 생성자(Default Constructor) : 개발자가 명시적으로 생성자를 정의하지 않으면, 자바 컴파일러는 매개변수가 없는 기본 생성자를 제공합니다.
이 기본 생성자는 객체의 필드를 기본값으로 초기화합니다.
2. 매개변수가 있는 생성자(Parameterized Constructor) : 하나 이상의 매개변수를 받아 객체의 초기 상태를 세팅 할 수 있도록 해줍니다.
6.3 예제.
public class Person {
private String name;
private int age;
// 기본 생성자
public Person() {
this.name = "Unknown";
this.age = 0;
}
// 매개변수가 있는 생성자
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void displayInfo() {
System.out.println("Name: " + name + ", Age: " + age);
}
}
public class Test {
public static void main(String[] args) {
// 기본 생성자를 사용하여 객체 생성
Person person1 = new Person();
person1.displayInfo(); // 출력: Name: Unknown, Age: 0
// 매개변수가 있는 생성자를 사용하여 객체 생성
Person person2 = new Person("Jhon", 25);
person2.displayInfo(); // 출력: Name: Jhon, Age: 25
}
}
이 예제에서 ‘Person’ 클래스는 두 가지 유형의 생성자를 가집니다.
하나는 매개변수가 없어 기본값으로 객체를 초기화하고, 다른 하나는 이름과 나이를 받아 객체를 초기화합니다.
📝 정리.
생성자를 사용함으로써 클래스의 인스턴스가 유효한 상태로 시작될 수 있도록 보장하며, 필요한 초기 설정을 자동으로 수행할 수 있습니다.
이는 객체 지향 프로그래밍에서 객체의 무결성을 유지하는 중요한 방법입니다.
7. this 키워드와 this() 생성자 호출.
자바에서 ‘this’ 키워드와 ‘this()’ 생성자 호출은 객체 자신을 참조하고 객체의 생성자를 호출하는 데 사용되는 중요한 요소입니다.
이들은 객체 내부에서 사용되며, 클래스의 멤버(필드, 메서드, 생성자)와 관련된 동작을 명확히 하는 데 유용합니다.
7.1 this 키워드.
‘this’ 키워드는 현재 객체, 즉 메서드나 생성자를 호출하는 인스턴스를 참조하는 데 사용됩니다.
주로 다음과 같은 상황에서 사용됩니다.
1. 필드와 매개변수 이름이 같을 때 구분 : 메서드나 생성자의 매개변수와 클래스의 필드 이름이 같을 때, 필드와 매개변수를 구분하기 위해 사용됩니다.
2. 메서드 체이닝 : 객체의 메서드를 연속적으로 호출할 때 ‘this’ 를 반환함으로써 메서드 체이닝을 구현할 수 있습니다.
3. 현재 객체를 다른 메서드에 전달 : 현재 객체의 참조를 다른 메서드에 전달할 때 사용됩니다.
7.2 this() 생성자 호출.
‘this()’ 는 같은 클래스의 다른 생성자를 호출하는 데 사용됩니다.
주로 생성자 오버로딩이 있을 때, 중복 코드를 최소화하고, 하나의 생성자에서 다른 생성자를 호출하여 필드 초기화 등의 공통 작업을 중앙집중적으로 관리할 수 있게 해줍니다.
1. 생성자 오버로딩 처리 : 클래스에 여러 생성자가 있을 때, ‘this()’ 를 사용하여 한 생성자에서 다른 생성자를 호출함으로써 공통 코드를 재사용할 수 있습니다.
2. 코드 간결성 유지 : 필수적인 초기화 작업을 주 생성자에만 명시하고, 나머지 생성자는 이 주 생성자를 호출하게 함으로써 코드의 간결성을 유지합니다.
7.3 예제.
public class Rectangle {
private int width;
private int height;
// 주 생성자
public Rectangle(int width, int height) {
this.width = width; // 'this'로 필드와 매개변수 구분
this.height = height;
}
// 부 생성자
public Rectangle() {
this(10, 10) // 'this()' 로 다른 생성자 호출
}
public void displaySize() {
System.out.println("Width: " + this.width + ", Height: " + this.height);
}
}
public class Test {
public static void main(String[] args) {
Rectangle rect1 = new Rectangle(30, 40);
rect1.displaySize(); // 출력: Width: 30, Height: 40
Rectangle rect2 = new Rectangle();
rect2.displaySize(); // 출력: Width: 10, Height: 10
}
}
이 예제에서 ‘Rectangle’ 클래스는 두 개의 생성자를 가지고 있습니다.
기본 생성자는 ‘this()’ 를 사용하여 주 생성자를 호출하고, 주 생성자에서는 ‘this’ 키워드를 사용하여 클래스 필드와 생성자 매개변수를 구분합니다.
이렇게 ‘this’ 와 ‘this()’ 를 사용함으로써 코드의 중복을 줄이고, 초기화 로직을 하나의 생성자에 집중할 수 있습니다.
📝 정리.
이처럼 ‘this’ 와 ‘this()’ 는 자바에서 클래스의 인스턴스 자신을 참조하거나 클래스 내 다른 생성자를 호출하는 데 매우 유용한 도구입니다.
-
-
☕️[Java] 반복문
1️⃣ 반복문
1. for 반복문.
자바 프로그래밍에서 ‘for’ 반복문은 특정 조건을 만족하는 동안 코드 블록을 반복해서 실행하도록 설계된 제어 구조입니다.
‘for’ 문은 초기화, 조건 검사, 반복 후 실행할 작업(일반적으로 증감)을 한 줄에 명시하여 코드의 가독성과 관리를 용이하게 합니다.
이는 반복 실행이 필요한 많은 상황에서 유용하게 사용됩니다.
1.2 for 반복문의 기본 구조.
‘for’ 문의 기본 구조는 다음과 같습니다.
for (초기화; 조건; 증감) {
// 반복해서 실행할 코드
}
초기화 : 반복문이 시작할 때 한 번 실행되는 부분으로, 반복문의 제어 변수를 초기 설정합니다.
조건 : 이 조건이 참(‘true’) 인 동안 반복문 내의 코드가 실행 됩니다. 조건이 거짓(‘false’)이 되면 반복문은 종료됩니다.
증감 : 각 반복의 끝에서 실행되며, 주로 제어 변수의 값을 증가시키거나 감소시키는데 사용됩니다.
1.3 for 반복문의 예시
기본 예시
for (int i = 0; i < 5; i++) {
System.out.println("i의 값은: " + i);
}
이 예제에서는 ‘i’ 를 0부터 시작하여 ‘i’ 가 5미만일 동안 반복하여, 매 반복마다 ‘i’ 를 1씩 증가시킵니다.
따라서 “i의 값은: 0” 부터 “i의 값은: 4” 까지 총 다섯 번의 출력을 하게 됩니다.
확장된 예시: 다중 제어 변수
for (int i = 0, j = 10; i < j; i++, j--) {
System.out.println("i = " + i + ". j = " + j);
}
이 예제에서는 두 개의 제어 변수 ‘i’ 와 ‘j’ 를 사용합니다.
‘i’ 는 증가하고 ‘j’ 는 감소하며, ‘i’ 가 ‘j’ 와 같거나 ‘j’ 보다 크게 되면 반복이 종료됩니다.
이런 패턴은 복잡한 반복 조건이 필요한 경우 유용하게 사용됩니다.
1.4 사용 사례
‘for’ 반복문은 배열이나 컬렉션과 같은 데이터 구조를 순회할 때 매우 유용합니다.
예를 들어, 배열의 모든 요소를 처리하거나 조작할 때 자주 사용됩니다.
int[] numbers = {1,2,3,4,5};
for (int i = 0; i < numbers.length; i++) {
System.out.println("배열 요소: " + numbers[i]);
}
여기서 ‘numbers.length’ 는 배열의 길이를 반환하며, ‘i’ 는 0에서 시작하여 배열의 크기 미만이 될 때까지 증가하면서 배열의 각 요소에 접근합니다.
📝 정리
‘for’ 반복문은 코드를 간결하게 하면서 반복적인 작업을 효과적으로 처리할 수 있도록 도와줍니다.
2. while 반복문.
자바 프로그래밍에서 ‘while’ 반복문은 특정 조건이 참(‘true’)인 동간 주어진 코드 블록을 반복적으로 실행하는 구조 입니다.
‘while’ 문은 주로 반복 횟수가 불확실할 때 또는 반복 횟수를 사전에 정확히 알 수 없을 때 사용됩니다.
2.1 while 반복문의 기본 구조.
‘while’ 문의 기본 구조는 다음과 같습니다.
while (조건) {
// 조건이 참인 동안 반복 실행될 코드
}
여기서 ‘조건’ 은 각 반복 이전에 평가되며, 이 조건이 참(‘true’)일 때 반복 블록 내의 코드가 실행됩니다. 조건이 거짓(‘false’)이 되면 반복문은 종료됩니다.
2.2 while 반복문 예시.
간단한 예
int i = 0;
while (i < 5) {
System.out.println("i의 값은: " + i);
i++; // i 값을 증가시켜 조건이 eventually 거짓이 되도록 함
}
이 예제에서는 ‘i’ 가 0부터 시작하여 5미만인 동안 반복문을 실행합니다.
반복문 내에서 ‘i’ 를 1씩 증가시켜 eventually 조건이 거짓이 되도록 합니다.
결과적으로 ‘i’ 의 값은 0부터 4까지 콘솔에 출력됩니다.
사용자 입력 받기
‘while’ 문은 사용자 입력을 받고, 그 입력에 따라 반복을 계속할지 여부를 결정할 때 유용하게 사용될 수 있습니다.
예를 들어, 사용자가 특정 문자를 입력할 때까지 입력을 계속 받는 프로그램은 다음과 같이 작성할 수 있습니다.
Scanner scanner = new Scanner(System.in);
String input = "";
while (!input.equals("종료")) {
System.out.println("문자열을 입력하세요. 종료하려면 '종료'를 입력하세요: ");
input = scanner.nextLine();
}
scanner.close();
2.3 주의사항.
‘while’ 반복문을 사용할 때는 반복문 내에서 조건이 eventually(결국) 거짓이 될 수 있도록 조치를 취해야 합니다.
그렇지 않으면, 조건이 항상 참으로 평가될 경우 무한 후프에 빠질 수 있습니다.
따라서 조건 변수를 적절히 조작하거나 적절한 로직을 구현하여 반복문이 적절한 시점에 종료될 수 있도록 해야 합니다.
📝 정리.
‘while’ 문은 그 구조가 간단하고 유연하여, 특정 조건 하에 반복 실행을 해야 할 때 매우 유용한 프로그래밍 도구입니다.
3. do-while 반복문.
자바 프로그래밍에서 ‘do-while’ 반복문은 조건을 검사하기 전에 최소 한 번은 코드 블록을 실행하는 반복문입니다.
이 구조는 ‘while’ 반복문과 비슷하지만, 조건의 참/거짓 여부에 관계없이 최소한 처음에는 반복문 내의 코드를 실행한다는 점이 다릅니다.
‘do-while’ 문은 주로 사용자 입력을 처리하거나, 조건이 반복문의 실행 후에 결정되어야 할 때 유용합니다.
3.1 do-while 반복문의 기본 구조.
‘do-while’ 문의 기본 구조는 다음과 같습니다.
do {
// 최소 한 번은 실행될 코드
} while (조건);
여기서 ‘조건’ 은 반복문의 끝에서 평가됩니다.
조건이 참(‘true’)이면, 코드 블록이 반복적으로 실행됩니다. 조건이 거짓(‘false’)이면, 반복이 종료됩니다.
3.2 do-while 반복문의 예시.
기본 예
int i = 0;
do {
System.out.println("i의 값은: " + i);
i++;
} while (i < 5);
이 예제에서는 ‘i’ 가 0부터 시작하여 ‘i < 5’ 인 동안 반복합니다.
‘do-while’ 문은 ‘i’ 의 초기 값에 상관없이 최소 한 번은 “i의 값은: 0”을 출력하고 시작합니다. 그 후 ‘i’ 가 5미만인 동안 계속해서 반복됩니다.
사용자 입력 받기
사용자로부터 입력을 받고, 특정 조겅(“종료” 문자열 입력)을 만족할 때까지 계속 입력을 받는 프로그램을 구현할 때 ‘do-while’ 문이 유용하게 사용됩니다.
Scanner scanner = new Scanner(System.in);
String input;
do {
System.out.println("문자열을 입력하세요. 종료하려면 '종료'를 입력하세요:");
input = scanner.nextLine();
} while (!input.equals("종료"));
scanner.close();
이 코드는 사용자가 “종료”를 입력할 때까지 계속해서 입력을 받습니다.
입력을 받는 동작은 최소 한 번은 실행되며, 이는 ‘do-while’ 문이 최조 실행을 보장하기 때문입니다.
3.3 주의사항.
‘do-while’ 반복문을 사용할 때, 조건을 적절히 설정하여 반복문이 적절한 시점에 종료되도록 해야 합니다. 그렇지 않으면 무한 루프에 빠질 수 있습니다.
또한, 조건 검사가 반복문의 끝에서 이루어지므로, 조건이 매우 빨리 거짓이 되어도 코드 블록이 한 번은 실행됨을 기억해야 합니다.
📝 정리.
‘do-while’ 문은 조건이 반복 블록 실행 후에만 알 수 있거나, 반복 블록을 적어도 한 번은 실행해야 하는 경우 특히 유용한 도구입니다.
4. continue.
자바 프로그래밍에서 ‘continue’ 문은 반복문 내에서 사용되며, 그것이 실행될 때 현재 반복의 나머지 부분을 건너뛰고 즉시 다음 반복으로 넘어가도록 합니다.
이를 통해 특정 조건에서 반복문의 다음 순환을 즉시 시작할 수 있게 해줍니다.
‘continue’ 는 주로 반복문 내에서 특정 조건에 대한 예외 처리나 불필요한 처리를 건너뛰기 위해 사용됩니다.
4.1 continue 기본 사용법
‘continue’ 문은 주로 ‘for’, ‘while’, 또는 ‘do-while’ 반복문 내에서 사용됩니다.
간단하게는 ‘continue’ 를 실행하면 반복문의 현재 순회에서 남은 코드를 실행하기 않고, 다음 반복으로 진행합니다.
4.2 continue 예시
‘for’ 반복문에서의 사용
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
continue; // 짝수인 경우, 출력을 건너뛰고 다음 반복으로 넘어감
}
System.out.println(i); // 홀수만 출력
}
이 코드는 0부터 9까지의 숫자 중에서 홀수만 출력합니다.
‘i’ 가 짝수일 경우, ‘continue’ 문이 실행되어 ‘System.out.println(i);’ 줄을 건너뛰고 다음 반복으로 넘어갑니다.
‘while’ 반복문에서의 사용
int i = 0;
while (i < 10) {
i++;
if (i % 2 == 0) {
continue; // 짝수인 경우, 아래의 출력을 건너뛰고 다음 반복으로 넘어감
}
System.out.println(i); // 홀수만 출력
}
이 예제에서도 ‘continue’ 는 짝수를 확인하는 조건에서 참일 경우 나머지 코드를 건너뛰고 다음 반복을 계속 진행하도록 합니다.
4.3 continue 특징 및 주의사항.
‘continue’ 문은 현재 수행 중인 반복의 나머지 부분을 건너뛰고, 반복문의 조건 검사로 직접 이동하여 다음 반복을 시작합니다.
‘continue’ 문을 사용할 때는 반복문이 무한 루프에 빠지지 않도록 주의해야 합니다.
예를 들어, ‘continue’ 문이 반복문의 변수 값을 변경하는 코드를 건너뛰면 그 변수의 값이 업데이트되지 않아 무한 루프가 발생할 수 있습니다.
‘continue’ 는 루프의 흐름을 제어하고, 코드의 읽기 어려움을 증가시킬 수 있으므로, 사용할 때는 명확한 이유가 있어야 합니다.
📝 정리.
‘continue’ 문은 코드를 보다 효율적으로 만들고, 필요 없는 조건을 빠르게 건너뛸 수 있게 도와줍니다.
그러나 코드의 가독성과 유지 관리에 영향을 줄 수 있으므로 신중하게 사용해야 합니다.
5. break문.
자바 프로그래밍에서 ‘break’ 문은 반복문(‘for’, ‘while’, ‘do-while’) 또는 ‘switch’ 문에서 현재 블록의 실행을 즉시 종료하고, 해당 블록의 바깥으로 제어를 이동시키는 역할을 합니다.
이는 반복문 또는 ‘switch’ 문 내에서 특정 조건을 만족할 때 추가적인 처리 없이 루프나 선택 구조를 벗어나기 위해 사용됩니다.
5.1 break 기본 사용법.
반복문에서의 사용 : ‘break’ 를 사용하여 무한 루프를 종료하거나 특정 조건이 만족될 때 반복문을 조기에 종료할 수 있습니다.
‘switch’ 문에서의 사용 : 각 ‘case’ 블록 뒤에 ‘break’ 를 사용하여 ‘switch’ 문을 종료하고, 다음 ‘case’ 로 넘어가지 않도록 합니다.
5.2 break 예시
반복문에서의 ‘break’
for (int i = 0; i < 10; i++) {
if (i == 5) {
break; // i가 5가 되면 for 루프를 종료
}
System.out.println(i);
}
이 코드에서는 ‘i’ 가 5에 도달하면 ‘break’ 문이 실행되어 ‘for’ 루프가 즉시 종료됩니다.
결과적으로, 0부터 4까지의 숫자만 출력됩니다.
‘while’ 반복문에서의 ‘break’
int i = 0;
while (true) { // 무한 루프
if (i == 5) {
break; // i가 5가 되면 while 루프를 종료
}
System.out.println(i);
i++;
}
이 예제에서도 ‘i’ 가 5일 때 ‘break’ 문을 사용하여 무한 루프를 종료합니다.
‘switch’ 문에서의 ‘break’
int number = 2;
switch (number) {
case 1:
System.out.println("Number is 1");
break;
case 2:
System.out.println("Number is 2");
break;
case 3:
System.out.println("Number is 3");
break;
default:
System.out.println("Number is not 1, 2, or 3");
}
‘switch’ 문에서 ‘number’ 가 2일 때 해당하는 ‘case’ 블록이 실행되고, ‘break’ 문으로 인해 ‘switch’ 문을 벗어나게 됩니다.
5.3 break문 특징 및 주의사항
‘break’ 문은 코드 실행을 즉시 중단시키므로, 효과적인 프로그램 흐름 제어를 가능하게 합니다.
반복문이나 ‘switch’ 문 내에서만 ‘break’ 문을 사용할 수 있습니다.
‘break’ 문을 사용할 때는 코드의 흐름을 명확히 이해하고 있어야 하며, 무분별한 사용은 코드의 가독성과 유지보수를 어렵게 만들 수 있습니다.
📝 정리.
‘break’ 는 코드의 복잡성을 줄이고 특정 조건에서 즉시 반복문을 종료할 수 있는 강력한 도구입니다.
그러나 그 사용은 코드의 구조를 명확하게 유지하는 방식으로 신중하게 이루어져야 합니다.
6. for-each문.
자바 프로그래밍에서 ‘for-each’ 문, 또는 강화된 ‘for’ 문(enhanced for loop)은 배열이나 컬렉션 프레임워크에 저장된 각 요소를 순회하기 위해 사용되는 구문입니다.
기존의 ‘for’ 문보다 간결하며, 코드를 읽고 작성하기가 더 쉬워 배열이나 컬렉션의 모든 요소에 접근할 때 일반적으로 권장되는 방식입니다.
6.1 for-each문 기본 구조.
‘for-each’ 문의 기본 구조는 다음과 같습니다.
for (타입 변수명 : 배열 또는 컬렉션) {
// 변수명을 사용한 코드
}
여기서 ‘타입’ 은 배열 또는 컬렉션에 저장된 요소의 타입을 말하고, ‘변수명’ 은 반복되는 각 요소를 참조하는 데 사용되는 변수 이름입니다.
‘배열 또는 컬렉션’ 은 순회할 배열이나 컬렉션 객체를 지정합니다.
6.2 for-each문 예시
배열 사용 예
int[] numbers = {1,2,3,4,5};
for(int number: numbers) {
System.out.println(number);
}
이 코드에서 ‘for-each’ 문은 ‘numbers’ 배열의 모든 요소를 순회합니다.
각 반복에서 ‘number’ 변수에 배열의 요소가 할당되며, 그 값을 출력합니다.
컬렉션 사용 예
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
for (String name : names) {
System.out.println(name);
}
이 예에서는 ‘names’ 리스트의 모든 요소를 순회합니다.
각 요소는 ‘name’ 변수에 할당되고, ‘System.out.println’ 을 통해 출력됩니다.
6.3 for-each문의 장점.
가독성 향상: ‘for-each’ 문은 간결하고 이해하기 쉬워, 코드의 가독성을 크게 향상시킵니다.
오류 감소: 전통적인 ‘for’ 문에서 발생할 수 있는 인덱스 관련 실수나 경계 조건 오류를 방지할 수 있습니다.
향상된 추상화: 컬렉션의 내부 구조나 크기를 몰라도 각 요소에 접근할 수 있습니다.
6.4 for-each문의 제한사항.
컬렉션 수정 불가: ‘for-each’ 문을 사용하는 동안 컬렉션을 수정할 수 없습니다.
예를 들어, 순회 중인 컬렉션에서 요소를 추가하거나 제거할 수 없습니다.
인덱스 접근 불가: ‘for-each’ 문은 각 요소에 대한 인덱스를 제공하지 않습니다.
특정 인덱스의 요소에 접근하거나 인덱스를 활용한 복잡한 로직이 필요한 경우에는 전통적인 ‘for’ 문을 사용해야 합니다.
📝 정리.
‘for-each’ 문은 자바에서 컬렉션과 배열을 효율적으로 처리할 수 있는 강력하고 사용하기 쉬운 도구입니다.
이를 통해 코드를 더욱 간결하고 안전하게 만들 수 있습니다.
-
-
☕️[Java] 조건문
1️⃣ 조건문
1. if문.
자바 프로그래밍에서 ‘if’ 문은 조건부 실행을 제어하는 기본적인 제어 구문입니다.
이를 통해 프로그램은 주어진 조건이 참(‘true’)인지 거짓(‘false’)인지에 따라 다른 행동을 취할 수 있습니다.
1.1 if문 기본 구조.
‘if’ 문의 기본 구조는 다음과 같습니다.
if (조건) {
// 조건이 참일 때 실행될 코드
}
여기서 ‘조건’ 은 boolean 타입의 표현식으로, 평가 결과가 ‘true’ 또는 ‘false’ 가 됩니다.
조건이 ‘true’ 일 때만 중괄호 ’{}’ 내부의 코드가 실행됩니다.
1.2 예시
예를 들어, 사용자의 나이가 성인 기준을 만족하는지를 확인하는 코드는 다음과 같습니다.
int age = 20;
if (age >= 18) {
System.out.println("성인입니다.");
}
이 코드에서 ‘age >= 18’ 이라는 조건이 참이면 “성인입니다.” 라는 메시지를 출력합니다.
1.3 ‘else’와 ‘else if’ 확장
‘if’ 문은 종종 ‘else’ 와 ‘else if’ 와 함께 사용되어 보다 복잡한 조건 로직을 구현할 수 있습니다.
if (조건1) {
// 조건1이 참일 때 실행될 코드
} else if (조건2) {
// 조건1이 거짓이고 조건2가 참일 때 실행될 코드
} else {
// 위의 모든 조건이 거짓일 때 실행될 코드
}
예를 들어, 점수에 따라 학점을 출력하는 코드는 다음과 같습니다.
int score = 85;
if (score >= 90) {
System.out.println("학점 A");
} else if (score >= 80) {
System.out.println("학점 B");
} else if (score >= 70) {
System.out.println("학점 C");
} else {
System.out.println("학점 D");
}
이 예제에서 ‘score’ 변수의 값에 따른 다른 학점을 출력합니다.
‘if’, ‘else if’, ‘else’ 구문은 점수 범위에 따라 조건적으로 실행되며, 가장 먼저 만족하는 조건의 블록만 실행됩니다.
📝 정리.
‘if’ 문은 프로그래밍에서 결정을 내리는 데 필수적인 구조이며, 다양한 조건에 따라 코드의 실행 흐름을 제어하는 데 사용됩니다.
2. switch문.
자바 프로그래밍에서 ‘switch’ 문은 다수의 조건 중 하나를 선택해 실행할 때 사용하는 조건문입니다.
이는 ‘if-else’ 조건문의 대안으로, 변수의 값에 따라 여러 실행 경로 중 하나를 선택할 수 있도록 해줍니다.
‘switch’ 문은 특히 특정 변수가 취할 수 있는 명확한 값들을 기반으로 다양한 케이스를 처리할 때 유용하게 사용됩니다.
2.1 switch문의 기본 구조.
‘switch’ 문의 기본 구조는 다음과 같습니다.
switch (표현식) {
case 값1:
// 표현식 결과가 값1과 일치할 때 실행할 코드
break;
case 값2:
// 표현식 결과가 값2와 일치할 때 실행할 코드
break;
// 추가적인 case들을 더 정의할 수 있습니다.
default:
// 어떤 case도 일치하지 않을 때 실행할 코드
}
2.2 switch 문의 주요 특징.
1. 표현식 은 주로 정수, 문자형 또는 열거형(enum) 데이터를 사용합니다. 자바 7 이상에서는 문자열(String)도 지원합니다.
2. case 라벨은 ‘switch’ 문 내에서 표현식의 결과와 일치하는 값을 가지며, 해당 값에 대한 실행 코드를 포함합니다.
3. break 문은 ‘switch’ 문을 종료하고 다음 코드로 넘어가도록 합니다. ‘break’ 가 없으면 다음 ‘case’ 로 계속 진행되어 “fall-through” 현상이 발생합니다.
4. default 섹션은 선택적으로 사용되며, 어떤 ‘case’ 도 일치하지 않을 때 실행됩니다.
2.3 switch 문 예시.
학생의 점수에 따라 학점을 부여하는 간단한 예를 들어보겠습니다.
int score = 92;
String grade;
switch (score / 10) {
case 10:
case 9:
grade = "A";
break;
case 8:
grade = "B";
break;
case 7:
grade = "C";
break;
case 6:
grade = "D";
break;
default:
greade = F;
}
System.out.println("학점: " + grade);
이 코드에서 ‘score / 10’ 의 결과값에 따라 다른 ‘case’ 블록이 실행됩니다.
‘92/10’ 은 ‘9’ 이므로, ‘grade’ 는 “A” 가 됩니다.
각 ‘case’ 는 ‘break’ 문으로 종료되므로, 해당 ‘case’ 실행 후, ‘switch’ 문을 벗어납니다.
📝 정리.
‘switch’ 문은 코드의 가독성을 높이고, 많은 조건 분기를 간결하게 처리할 수 있는 방법을 제공합니다.
이는 특히 각 조건이 명확할 때 더욱 유용하며, 코드의 구조를 명확하게 표현할 수 있습니다.
-
☕️[Java] 여러가지 연산자(1)
1️⃣ 각각의 연산자에 대한 이해
1. 항과 연산자.
1.1 단항 연산자(Unary Operator).
자바 프로그래밍에서 단항 연산자(Unary Operator)는 오직 한 개의 피연산자(operand)를 가지고 연산을 수행하는 연산자를 말합니다.
이들은 변수나 값에 직접 적용되며, 표현식의 결과를 반환합니다.
단항 연산자는 특히 간단한 수학 연산, 값의 부정, 또는 값의 증감 등에서 유용하게 사용됩니다.
1.2 자바에서 사용되는 주요 단항 연산자.
1. 부정 연산자('!')
불리언 값을 반전시킵니다.
예를 들어, '!true' 는 'false' 가 됩니다.
2. 단항 플러스 및 마이너스 연산자('+', '-')
'+' 는 일반적으로 숫자의 부호를 그대로 두지만, 명시적으로 사용할 수 있습니다.
'-' 는 숫자의 부호를 반전시킵니다.
예를 들어, '-5' 는 양수 '5' 를 음수로 변환합니다.
3. 증가 및 감소 연산자('++', '--')
'++' 연산자는 변수의 값을 1만큼 증가시킵니다.
'--' 연산자는 변수의 값을 1만틈 감소시킵니다.
이 연산자들은 전위(prefix) 형태(예: '++x')와 후위(postfix) 형태(예: 'x++')로 사용될 수 있습니다.
전위 형태는 변수를 증가시키고 표현식의 값을 반환하기 전에 증가된 값을 사용하고, 후위 형태는 표현식의 값을 반환한 후 변수를 증가시킵니다.
4. 비트 반전 연산자('~')
정수형 변수는 모든 비트를 반전시킵니다.
예를 들어, '~00000000' 은 '11111111' 이 됩니다.
이는 정수에 대해 비트 단위 NOT 연산을 수행합니다.
1.3 예제 사용.
public class UnaryDemo {
public static void main(String[] args) {
boolean a = false;
System.out.println(!a); // true
int num = 5;
System.out.println(-num); // -5
System.out.println(++num); // 6
System.out.println(num++); // 6, 그리고 num이 7이 됨
System.out.println(--num); // 6
System.out.println(num--); // 6, 그리고 num이 5가 됨
int b = 0b00000000; // 이진수로 0
System.out.println(~b); // 모든 비트가 1로 반전
}
}
이 예제에서는 다양한 단항 연산자들의 사용 방법과 그 효과를 보여줍니다.
단항 연산자들은 자바 프로그래밍에서 변수를 조작하거나 특정 연산을 더 간결하게 수행하는데 매우 유용합니다.
2.1 이항 연산자(Binary Operator)
자바 프로그래밍에서 이항 연산자(Binary Operator)는 두 개의 피연산자(operand)를 취해 연산을 수행하고 결과를 반환하는 연산자를 말합니다.
이항 연산자는 수학적 계산, 논리 비교, 값의 할당 등 다양한 작업에 사용됩니다.
2.2 자바에서 사용되는 주요 이항 연산자의 종류.
1. 산술 연산자.
’+’ (덧셈)
’-‘ (뺄셈)
‘*‘ (곱셈)
’/’ (나눗셈)
’%’ (나머지)
2. 비교 연산자.
’==’ (동등)
’!=’ (부등)
’>’ (크다)
’<’ (작다)
’>=’ (크거나 같다)
’<=’ (작거나 같다)
3. 논리 연산자.
‘&&’ (논리적 AND)
**’
‘** (논리적 OR)
4. 비트 연산자
‘&’ (비트 AND)
**’
‘** (비트 OR)
’^’ (비트 XOR)
5. 할당 연산자
’=’ (기본 할당)
’+=’, ‘-=’, ‘*=’, ‘/=’, ‘%=’ (복합 할당 연산자)
**‘&=’, ‘
=’, ‘^=’, ‘«=’, ‘»=’, ‘»>=’** (비트 복합 할당 연산자)
6. 시프트 연산자
’«‘ (왼쪽 시프트)
’»‘ (오른쪽 시프트, 부호 유지)
’»>’ (오른쪽 시프트, 부호 비트 없음)
2.3 예제 코드
public class BinaryOperatorsExample {
public static void main(String[] args) {
int a = 10, b = 5;
int sum = a + b; // 15
int difference = a - b; // 5
boolean isEqual = (a == b); // false
boolean isGreater = (a > b); // true
int bitAnd = a & b; // 비트 AND 연산
int shiftedLeft = a << 2; // 왼쪽으로 2 비트 시프트
System.out.println("Sum: " + sum);
System.out.println("Difference: " + difference);
System.out.println("Is Equal: " + isEqual);
System.out.println("Is Greater: " + isGreater);
System.out.println("Bitwise AND: " + bitAnd);
System.out.println("Left Shifted: " + shiftedLeft);
}
}
이항 연산자들은 기본적인 산술 연산부터 복잡한 논리 연산에 이르기까지 프로그래밍에서 광범위하게 사용됩니다.
이를 통해 효과적으로 데이터를 조작하고, 조건을 평가하며, 복잡한 문제를 해결할 수 있습니다.
3.1 삼항 연산자(Ternary Operator).
자바 프로그래밍에서 삼항 연산자(Ternary Operator), 또는 조건 연산자(Conditional Operator)라고 불리는 ’?:’ 는 세 개의 피연산자를 사용하는 유일한 연산자입니다.
이 연산자는 간결한 조건문을 구현할 때 사용되며, 간단한 조건식을 기반으로 두 가지 선택지 중 하나를 반환합니다.
3.2 삼항 연산자의 구조.
조건식 ? 값1 : 값2
1. 조건식 : 이 부분은 ‘true’ 또는 ‘false’ 를 반환하는 불리언 식입니다.
2. 값2 : 조건식이 ‘true’ 일 때 반환됩니다.
3. 값3 : 조건식이 ‘false’ 일 때 반환됩니다.
조건식의 평가 결과에 따라 ‘값1’ 또는 값2 중 하나가 결과값으로 선택되어 반환됩니다.
이 연산자는 일반적으로 간단한 조건에 따라 변수에 값을 할당하거나 특정 표현식의 결과를 결정할 때 유용하게 사용됩니다.
3.3 예제 사용.
public class TernaryExample {
public static void main(String[] args) {
int a = 10, b = 5;
int max = (a > b) ? a : b; // a와 b 중 큰 값을 max에 할당.
System.out.println("Max value: " + max);
String response = (a > b) ? "a is greater than b" : " b is greater or equal to a";
System.out.println(response);
}
}
이 예제에서 삼항 연산자를 사용하여 두 숫자 중 더 큰 숫자를 결정하고, 문자열 메시지도 조건에 따라 선택합니다.
이러한 사용은 코드를 더 간결하게 만들고, ‘if-else’ 구조를 보다 간단하게 대체할 수 있게 해 줍니다.
삼항 연산자는 그 효율성과 간결함 때문에 자바 프로그래밍에서 자주 사용되는 유용한 도구입니다.
그러나 복잡한 로직이나 여러 조건이 연속적으로 필요한 경우에는 가독성을 위해 전통적인 ‘if-else’ 문을 사용하는 것이 더 나을 수 있습니다.
-
☕️[Java] 여러가지 연산자(2)
1️⃣ 비트 연산자에 대한 이해
1. 2진법.
자바 프로그래밍에서의 이진법은 컴퓨터의 지본 숫자 시스템을 참조하는 것입니다.
컴퓨터는 데이터를 0과 1의 형태, 즉 이진수로 처리합니다.
자바에서도 이러한 이진법을 사용하여 데이터를 저장, 처리하며 다양한 연산을 수행할 수 있습니다.
1.1 자바에서 2진법을 사용하는 예.
1. 이진 리터럴 : 자바 7 이상부터는 정수를 이진 리터럴로 직접 표현할 수 있습니다.
예를 들어, ‘int x = 0b1010;’ 은 이진수 ‘1010’ 이고, 십진수로 10입니다.
2. 비트 연산자 : 자바는 비트 연산을 수행할 수 있는 여러 연산자를 제공합니다.
예를 들어, 다음과 같습니다.
'&' (AND 연산자)
'|' (OR 연산자)
'^' (XOR 연산자)
'~' (NOT 연산자)
'<<' (왼쪽 시프트)
'>>' (오른쪽 시프트)
'>>>' (부호 없는 오른쪽 시프트)
이들 연산자는 주로 효율적인 수치 계산, 저수준 프로그래밍, 암호와 작업 등에 사용됩니다.
3. 이진 데이터 조작 : 파일이나 네트워크를 통해 바이트 단위로 데이터를 읽고 쓸 때, 이진 형식으로 데이터를 처리합니다.
자바에서는 'byte' 자료형을 이용하여 이진 데이터를 직접 다룰 수 있습니다.
📝 정리
이진법을 사용하는 주된 이유는 컴퓨터 하드웨어가 전기 신호로 작동하기 때문에 0과 1, 즉 이진 상태를 나타내는 전기의 켜짐과 꺼짐 상태로 모든 데이터를 표현하기 편리하기 때문입니다.
이렇게 함으로써, 프로그래밍에서 더욱 직접적이고 효율적인 하드웨어 조작이 가능해집니다.
2. 2의 보수.
자바 프로그래밍에서의 2의 보수(2’s complement)는 음수를 표현하기 위한 방법입니다.
컴퓨터 시스템은 보통 이진법을 사용하여 데이터를 저장하고 처리하는데, 이진법에서 음수를 표현하기 위해 가장 널리 사용되는 방법이 2의 보수입니다.
2.1 2의 보수 생성 과정.
1. 원래 숫자의 이진 표현을 얻습니다.
예를 들어, 5의 이진 표현은 ‘0101’ 입니다.
2. 이진 표현의 모든 비트를 반전시킵니다.
즉, 0은 1로, 1은 0으로 변경합니다.
5의 경우 ‘0101’ 이 ‘1010’ 이 됩니다.
3. 반전된 값에 1을 더합니다.
이렇게 하면 ‘1011’ 이됩니다.
이렇게 생성된 ‘1011’ 은 -5를 나타냅니다.
이 방법은 자바를 포함한 대부분의 프로그래밍 언어와 컴퓨터 시스템에서 음수를 표현하는 표준 방법입니다.
2.2 2의 보수의 장점.
덧셈 연산만으로 뺄셈을 할 수 있습니다.
예를 들어, 5-5를 계산하려면 5와 -5의 2의 보수를 더하면 됩니다.
이진법으로는 ‘1010 + 1011 = 10000’ 이고, 최상위 비트(캐리 비트)는 무시합니다.
따라서 결과는 ‘0000’ 이 됩니다.
오버플로 처리가 간단합니다.
캐리 비트는 무시하면서 자연스럽게 오버플로를 처리할 수 있습니다.
2.3 자바에서의 활용.
자바에서는 정수형 타입(‘int’, ‘long’, ‘short’, ‘byte’) 이 이진법으로 2의 보수 형태로 저장되고 처리됩니다.
이는 자바의 모든 정수 연산에 내장된 메커니즘입니다.
예를 들어, 자바에서 ‘-5’ 를 선언하면 내부적으로는 ‘5’ 의 2의 보수인 ‘111…11011’ (32비트 시스템에서의 표현)으로 저장됩니다.
📝 정리
2의 보수 방식은 음수를 다루기 위한 효과적인 방법이며, 프로그래머가 별도의 조치를 취하지 않아도 시스템이 자동으로 처리해 주기 때문에 매우 편리합니다.
3. 비트 논리연산자.
자바 프로그래밍에서 비트 논리연산자는 비트 단위로 논리 연산을 수행하는 연산자입니다.
이들 연산자는 주로 정수 타입의 변수에 사용되며, 각 비트를 독립적으로 비교하여 결과를 반환합니다.
비트 논리연산자는 주로 저수준 프로그래밍, 효율적인 데이터 처리, 상태 플래그 관리, 암호화 등의 작업에 활용됩니다.
자바에서 사용되는 주요 비트 논리 연산자는 다음과 같습니다.
1. AND 연산자(‘&’) : 두 피연산자의 비트가 모두 1일 경우에만 결과의 해당 비트를 1로 설정합니다.
예를 들어, ‘5 & 3’ 은 이진수로 ‘0101 & 0011’ 을 계산하여 ‘0001’ 이 되므로, 결과는 ‘1’ 입니다.
**2. OR 연산자(‘
’) :** 두 피연산자 중 하나라도 비트가 1이면 결과의 해당 비트를 1로 설정합니다.
예를 들어, **‘5
3’** 은 이진수로 **‘0101
0011’** 을 계산하여 ‘0111’ 이 되므로, 결과는 ‘7’ 입니다.
3. XOR 연산자(‘^’) : 두 피연산자의 비트가 서로 다를 경우 결과의 해당 비트를 1로 설정합니다.
예를 들어, ‘5 ^ 3’ 은 이진수로 ‘0101 ^ 0011’ 을 계산하여 ‘0110’ 이 되므로, 결과는 ‘6’ 입니다.
4. NOT 연산자(‘~’) : 피연산자의 모든 비트를 반전시킵니다.(1은 0으로, 0은 1로).
예를 들어, ‘~5’ 는 이진수로 ‘~0101’ 을 계산하여 ‘…1010’(무한히 많은 1 다음에 1010)이 되고, 이는 보통 32비트 시스템에서 ‘-6’ 으로 해석됩니다.
5. 왼쪽 시프트(‘«’) : 모든 비트를 왼쪽으로 지정된 수만틈 이동시키고, 오른쪽은 0으로 채웁니다.
예를 들어.‘3 « 2’ 는 ‘0011’ 을 왼쪽으로 2비트 이동하여 ‘1100’ 이 되므로, 결과는 ‘12’ 입니다.
6. 오른쪽 시프트(‘»’) : 모든 비트를 오른쪽으로 지정된 수만큼 이동시키고, 왼쪽은 최상위 비트(부호 비트)의 값으로 채웁니다.
예를 들어, ‘-8 » 2’ 는 ‘11111000’ 을 오른쪽으로 2비트 이동하여 ‘11111110’ 이 되므로, 결과는 ‘-2’ 입니다.
7. 부호 없는 오른쪽 시프트(‘»>’) : 모든 비트를 오른쪽으로 지정된 수만큼 이동시키고, 왼쪽은 0으로 채웁니다. 이는 부호 비트를 무시하고, 순수하게 비트를 오른쪽으로 이동시키기 때문에 음수에 사용했을 때 결과가 달라집니다.
📝 정리
이러한 비트 논리 연산자들은 데이터의 특정 비트를 직접 조작할 필요가 있는 경우에 유용하며, 자바 프로그래밍에서 중요한 도구입니다.
-
-
☕️[Java] 변수와 자료형(4)
변수와 자료형(4)
1️⃣ 자료형에 대한 이해
1. List
자바 프로그래밍에서 List 는 일련의 요소를 저장하는 데 사용되는 순차적인 컬렉션을 나타냅니다.
이는 자바의 java.util.List 인터페이스를 통해 제공되며, 이는 주문된 컬렉션을 관리하기 위한 다양한 메소드를 제공합니다.
List 는 중복된 요소를 포함할 수 있고, 각 요소는 리스트 내에서 특정 위치를 가집니다.
사용자는 이 위치를 인덱스로 사용하여 리스트의 요소에 접근할 수 있습니다.
List 인터페이스의 주요 특징은 다음과 같습니다.
1. 순서 보장 : 리스트는 요소들이 추가된 순서를 유지하며, 각 요소는 특정 인덱스를 통해 접근할 수 있습니다.
2. 요소의 중복 허용 : 같은 값을 가진 요소를 여러 개 포함할 수 있습니다.
3. 동적 배열 : 리스트의 크기는 고정되어 있지 않고, 요소를 추가하거나 삭제함에 따라 동적으로 조절됩니다.
자바에서는 List 인터페이스를 구현하는 몇 가지 클래스가 있습니다.
가장 흔히 사용되는 구현체는 다음과 같습니다.
'ArrayList' : 내부적으로 배열을 사용하여 요소를 저장합니다.
요소의 추가와 인덱스를 통한 접근이 매우 빠르지만, 크기 조절이 필요할 때는 비용이 많이 들 수 있습니다.
'LinkedList' : 각 요소가 다음 요소에 대한 참조와 함께 저장되는 연결 리스트를 사용합니다.
요소의 추가와 삭제는 빠르지만, 인덱스를 통한 요소 접근은 시작부터 요소를 찾을 때까지 순차적으로 검색해야 하므로 시간이 더 걸립니다.
'Vector' : 'ArrayList' 와 비슷하지만, 다중 스레드 환경에서 안전하게 사용할 수 있도록 동기화된 메소드를 제공합니다.
📝 정리.
'List' 는 자바 컬렉션 프레임워크의 일부이며, 데이터를 관리하고 처리하는 데 매우 유용합니다.
프로그래머는 이러한 컬렉션을 사용하여 데이터를 유연하게 조작할 수 있습니다.
1.2 List의 주요 메서드.
자바의 'List' 인터페이스에는 여러 가지 중요한 메서드들이 포함되어 있으며, 이를 통해 리스트 내의 요소들을 조작하고 접근할 수 있습니다.
다음은 'List' 인터체이스에서 제공하는 몇 가지 주요 메서드들입니다.
'add(E e)': 리스트의 끝에 요소를 추가합니다.
'add(int index, E element)': 지정된 위치에 요소를 삽입합니다.
'addAll(Collection<? extends E> c)': 지정된 컬렉션의 모든 요소를 리스트의 끝에 추가합니다.
'addAll(int index, Collection<? extends E> c)': 지정된 위치부터 컬렉션의 모든 요소를 리스트에 추가합니다.
'clear()': 리스트에서 모든 요소를 제거합니다.
'contains(Object o)': 리스트가 특정 요소를 포함하고 있는지 확인합니다.
'get(int index)': 지정된 위치의 요소를 반환합니다.
'indexOf(Object o)': 주어진 요소의 첫 번째 인덱스를 반환합니다. 요소가 리스트에 없는 경우 -1을 반환합니다.
'lastIndexOf(Object o)': 주어진 요소의 마지막 인덱스를 반환합니다. 요소가 리스트에 없는 경우 -1을 반환합니다.
'isEmpty()': 리스트가 비어 있는지 확인합니다.
'iterator()': 리스트의 요소에 대한 반복자를 반환합니다.
'listIterator()': 리스트의 요소를 리스트 순서대로 반복하는 리스트 반복자를 반환합니다.
'remove(Object o)': 주어진 요소를 리스트에서 처음 발견되는 위치에서 제거하고, 그 결과를 반환합니다.
'remove(int index)': 지정된 위치에 있는 요소를 리스트에서 제거하고, 그 요소를 반환합니다.
'replaceAll(UnaryOperator<E> operator)': 주어진 연산자를 사용하여 리스트의 모든 요소를 대체합니다.
'size()': 리스트에 있는 요소의 수를 반환합니다.
'sort(Comparator<? super E> c)': 주어진 비교자를 사용하여 리스트를 정렬합니다.
'subList(int fromIndex, int toIndex)': 지정된 범위의 부분 리스트를 반환합니다.
'toArray()': 리스트 요소를 배열로 반환합니다.
📝 정리.
이 메서드들을 통해 리스트를 생성, 조회, 수정 및 관리하는 다양한 작업을 수행할 수 있습니다.
List 인터페이스를 사용함으로써 데이터를 효율적으로 처리하고 구조화할 수 있습니다.
2. Map
자바 프로그래밍에서 'Map' 은 키(key)와 값(value)의 쌍을 저장하는 객체입니다.
이는 키를 기반으로 빠르게 값을 검색할 수 있게 해주는 데이터 구조로, 각 키는 고유해야 합니다.(즉, 중복된 키를 가질 수 없습니다.)
'Map' 은 'java.util.Map' 인터페이스를 통해 정의되며, ‘HashMap‘, ‘TreeMap‘, ‘LinkedHashMap‘ 등 다양한 구현체를 가집니다.
자바의 ‘Map‘ 인터페이스는 키-값 쌍으로 데이터를 저장하고 관리하는 데 중점을 두는 데이터 구조로서, 특히 다음과 같은 주요 특징을 가지고 있습니다.
1. 키에 의한 값 접근 : ‘Map‘ 은 각 값에 고유한 키를 할당하며, 이 키를 사용하여 빠르게 해당 값을 검색할 수 있습니다.
이는 데이터베이스의 인덱스와 유사한 방식으로 작동합니다.
2. 키의 유일성 : 맵 내에서 모든 키는 고유해야 합니다.
즉, 같은 키가 두 번 이상 존재할 수 없으며, 새로운 키-값 쌍을 추가할 때 이미 존재하는 키를 사용하면 기존의 값이 새 값으로 대체됩니다.
3. 값의 중복 허용 : 키는 유일해야 하지만 값은 중복될 수 있습니다.
다른 키가 동일한 값을 가리킬 수 있습니다.
4. 순서의 유무 : 일반적인 ‘Map‘ 구현체들은 키-값 쌍의 순서를 보장하지 않습니다.
그러나 ‘LinkedHashMap‘ 과 같은 일부 구현체는 요소가 추가된 순서대로 반복할 수 있는 기능을 제공합니다.
‘TreeMap‘ 은 키에 따라 정렬된 순서를 유지합니다.
5. 비동기화 및 동기화 : 기본적으로 대부분의 ‘Map‘ 구현체는 동기화되지 않습니다.(‘HashMap‘). 이는 멀티 스레드 환경에서 동시 수정이 발생할 경우 안전하지 않을 수 있음을 의미합니다.
반면에 ‘Hashtable‘ 과 같은 구현체는 기본적으로 동기화가 되어 있어 멀티 스레드 환경에서 안전합니다.
또한, ‘Collections.synchronizeMap‘ 메소드를 사용하여 맵을 동기화된 맵으로 변환할 수 있습니다.
6. Null 허용 : 대부분의 ‘Map‘ 구현체는 키와 값으로 ‘null‘ 을 허용합니다.(‘HashMap‘, ‘LinkeHashMap‘).
하지만 ‘Hashtable‘ 은 ‘null‘ 키나 값을 허용하지 않으며, ‘TreeMap‘ 은 자연 정렬 또는 ‘Comparator‘ 가 ‘null‘ 을 처리할 수 있는 경우에만 ‘null‘ 키를 허용합니다.
📝 정리.
이러한 특징들로 인해 ‘Map‘ 은 다양한 애플리케이션에서 유연하고 효율적인 데이터 관리를 가능하게 합니다.
데이터를 쉽게 추가, 검색, 삭제할 수 있어 데이터 관리의 복잡성을 줄이고 성능을 최적화하는 데 기여합니다.
3. Generics.
자바 프로그래밍에서 제네릭스(Generics)는 클래스나 메소드에서 사용될 데이터 타입을 추상화하여 코드 작성 시점에는 구체적인 타입을 명시하지 않고, 객체 생성이나 메소드 호출 시점에 실제 사용할 타입을 지정할 수 있도록 하는 프로그래밍 기법입니다.
제네릭스(Generics)는 코드의 재사용성을 높이고, 타입 안정성을 강화하며, 캐스팅에 대한 오류 가능성을 줄이는 데 도움을 줍니다.
3.1 제네릭스(Generics)의 주요 특징.
1. 타입 안전성(Type Safety) : 제네릭스를 사용하면 컴파일 시점에 타입 체크가 가능하여, 실행 시점에서 발생할 수 있는 'ClassCastException' 과 같은 오류를 사전에 방지할 수 있습니다.
2. 재사용성(Reusability) : 하나의 코드를 다양한 타입에 대해 재사용할 수 있습니다.
예를 들어, 제네릭 클래스나 메소드를 정의하면, 다양한 타입의 객체를 저장하거나 처리하는 로직을 단 한번만 작성하여 여러 타입에 걸쳐 사용할 수 있습니다.
3. 코드 간결성(Code Clarity) : 캐스팅을 줄여 코드가 더욱 간결하고 읽기 쉬워집니다.
3.2 제네릭스의 기본 문법.
클래스 선언 : 클래스 이름 뒤에 '<T>' 를 추가하여 제네릭 클래스를 선언합니다.
‘T’ 는 타입 파라미터를 나타내며, 이는 클래스 내에서 사용될 데이터 타입을 대체하는 플레이스홀더 역할을 합니다.
public class Box<T> {
private T t; // T 타입의 객체를 위한 변수
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
메소드 선언 : 메소드 반환 타입 앞에 ’<T>‘ 를 추가하여 제네릭 메소드를 선언합니다.
public <T> T genericMethod(T t) {
return t;
}
제네릭 타입 제한(Bounded Type Parameters) : 특정 클래스의 하위 클래스만 타입 파라미터로 받도록 제한할 수 있습니다.
이는 'extends' 키워드를 사용하여 지정합니다.
public class Box<T extends Number> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
📝 정리.
제네릭스(Generics)를 사용함으로써 개발자는 보다 타입-안전하고 유지보수가 용이한 코드를 작성할 수 있으며, 실행 시 타입 관련 문제를 효과적으로 줄일 수 있습니다.
-
-
☕️[Java] 변수와 자료형(3)
변수와 자료형(3).
1️⃣ 자료형에 대한 이해.
1. String.
String 클래스는 불변(immutable)의 문자열을 다룹니다.
이는 한 번 생성된 String 객체의 내용이 변경될 수 없다는 것을 의미합니다.
문자열을 변경하려고 할 때마다 실제로 새로운 String 객체가 생성되고, 기존 객체는 변경되지 않습니다.
1.1 String의 주요 메소드.
charAt(int index) : 지정된 위치의 문자를 반환합니다.
concat(String str) : 현재 문자열의 끝에 지정된 문자열을 붙여 새로운 문자열을 반환합니다.
contains(CharSequence s) : 특정 문자열이 포함되어 있는지 확인합니다.
startsWith(String prefix) : 문자열이 특정 문자열로 시작하는지 확인합니다.
endsWith(String suffix) : 문자열이 특정 문자열로 끝나는지 확인합니다.
equals(Object anObject) : 문자열이 주어진 객체와 동일한지 비교합니다.
indexOf(int ch), indexOf(String str) : 주어진 문자 또는 문자열의 위치를 찾습니다.
length() : 문자열의 길이를 반환합니다.
replace(char oldChar, char newChar) : 문자열 중 일부 문자를 다른 문자로 대체합니다.
substring(int beginIndex, int endIndex) : 문자열의 부분을 추출합니다.
toLowerCase(), toUpperCase() : 문자열을 소문자 또는 대문자로 변환합니다.
trim() : 문자열의 앞뒤 공백을 제거합니다.
2. StringBuffer.
StringBuffer 클래스는 가변(mutable)의 문자열을 다루며, 문자열 변경 작업이 빈번할 때 사용하면 효율적입니다.
StringBuffer 객체는 내용을 직접 변경할 수 있어, 새로운 객체를 계속 생성하지 않아도 됩니다.
2.1 StringBuffer의 주요 메소드.
append(String str): 문자열의 끝에 주어진 문자열을 추가합니다.
delete(int start, int end): 문자열의 시작 인덱스부터 종료 인덱스 전까지의 부분을 삭제합니다.
deleteCharAt(int index): 지정된 위치의 문자를 삭제합니다.
insert(int offset, String str): 지정된 위치에 문자열을 삽입합니다.
replace(int start, int end, String str): 시작 인덱스부터 종료 인덱스 전까지의 문자열을 새로운 문자열로 대체합니다.
reverse(): 문자열의 순서를 뒤집습니다.
length(): 문자열의 길이를 반환합니다.
capacity(): 현재 버퍼의 크기를 반환합니다.
setCharAt(int index, char ch): 지정된 위치의 문자를 다른 문자로 설정합니다.
📝 정리.
StringBuffer 는 스레드에 안전(thread-safe)합니다, 즉 멀티스레드 환경에서 동시에 접근해도 안전하게 사용할 수 있습니다.
이는 내부적으로 메소드들이 동기화되어 있기 때문입니다.
반면, StringBuilder 는 StringBuffer 와 유사하지만 멀티스레드 환경에서의 동기화 자원이 없어 단일 스레드에서 더 빠르게 작동합니다.
이처럼 String 과 StringBuffer 는 각각의 특성에 맞게 선택하여 사용할 수 있으며, 성능과 사용상황에 따라 적절히 활용하면 됩니다.
3. Array.
자바 프로그래밍에서 배열(Array)은 동일한 타입의 여러 데이터를 연속적인 메모리 위치에 저장하기 위한 자료구조입니다.
배열은 고정된 크기를 가지며, 배열의 각 요소는 같은 데이터 타입을 가집니다.
배열을 사용하면 여러 데이터를 하나의 변수 이름으로 관리할 수 있어 코드를 간결하게 작성할 수 있습니다.
3.1 배열의 특징.
고정된 크기 : 배열은 생성 시 지정된 크기를 변경할 수 없습니다.
배열의 크기는 프로그램 실행 도중에 변경할 수 없으며, 더 많은 데이터를 저장해야 할 경우 새로운 배열을 생성하고 데이터를 복사해야 합니다.
인덱스 접근 : 배열의 각 요소는 인덱스를 통해 접근할 수 있습니다.
인덱스는 0부터 시작하여 배열의 크기 -1까지 번호가 할당됩니다.
동일 타입 : 모든 배열 요소는 동일한 데이터 타입을 가져야 합니다.
예를 들어, int 타입의 배열은 int 타입의 값만을 요소로 가질 수 있습니다.
3.2 배열의 선언과 초기화.
배열을 선언하고 사용하기 위해서는 다음 단계를 따라야 합니다.
1. 배열 선언 : 데이터 타입 뒤에 대괄호 [] 를 사용하여 배열을 선언합니다.
int[] myArray;
String[] stringArray;
2. 배열 생성 : new 키워드를 사용하여 배열을 생성하고, 배열의 크기를 지정합니다.
myArray = new int[10]; // 10개의 정수를 저장할 수 있는 배열
stringArray = new String[5]; // 5개의 문자열을 저장할 수 있는 배열
3. 배열 초기화 : 배열의 각 요소에 값을 할당합니다.
인덱스를 사용하여 접근할 수 있습니다.
myArray[0] = 50;
myArray[1] = 100;
stringArray[0] = "Hello";
stringArray[1] = "World";
3.3 배열 사용 예.
다음은 자바에서 int 배열을 선언, 생성, 초기화하는 예제 코드입니다.
public class ArrayExample {
public static void main(Stringp[] args) {
// 배열 선언과 동시에 생성
int[] numbers = new int[3];
// 배열 초기화
numbers[0] = 7;
numbers[1] = 20;
numbers[2] = 33;
// 배열 사용
System.out.println("첫 번째 숫자: " + numbers[0]);
System.out.println("두 번째 숫자: " + numbers[1]);
System.out.println("세 번째 숫자: " + numbers[2]);
}
}
📝 정리
이 예제에서는 numbers 라는 이름의 int 배열을 생성하고, 세 개의 정수를 저장한 후 출력합니다.
배열을 사용하는 이점 중 하나는 이처럼 단일한 이름으로 여러 데이터를 효율적으로 관리할 수 있다는 것입니다.
-
☕️[Java] 자바 - 변수와 자료형(2)
☕️ 자바 - 변수와 자료형(2)
1. 자료형에 대한 이해
자바 프로그래밍에서 사용되는 자료형은 크게 기본형(Primitive types) 과 참조형(Reference types) 두 가지로 나눌 수 있습니다.
각각의 자료형에 대해 설명드리겠습니다.
1️⃣ 기본형(Primitive types)
기본형 자료형은 실제 값을 저장하는 타입으로, 총 8가지가 있습니다.
정수형
byte : 8비트 정수형, 값의 범위는 -128에서 127까지.
short : 16비트 정수형, 값의 범위는 -32,768에서 32,767까지.
int : 32비트 정수형, 값의 범위는 약 -2.14억에서 2.14억까지.
long : 64비트 정수형, 값의 범위는 약 -9.22경에서 9.22경까지
실수형
float : 32비트 부동 소수점 형. 부정확할 수 있으며, 대략 6~7 자리의 정밀도를 가짐.
double : 64비트 부동 소수점 형. float보다 더 정밀하며, 대략 15자리의 정밀도를 가짐.
문자형
char : 단일 16비트 유니코드 문자를 저장.
논리형
boolean : true 또는 false 값만을 가짐.
2️⃣ 참조형(Reference types)
참조형 자료형은 객체의 참조(메모리 주소)를 저장합니다.
기본형과 달리 메모리의 특정 위치를 가리키는 포인터를 저장하므로, 객체의 크기에 관계없이 참조 변수 크기는 항상 일정합니다.
참조형의 예를 들면 다음과 같습니다.
클래스(Class)
예: String, Integer, File 등
인터페이스(Interface)
예: List, Map, Serializable 등
배열(Array)
예: int[], double[], String[] 등
1.1 인터페이스(Interface)?
자바에서 인터페이스(Interface)는 특정 클래스가 구현해야 할 메소드를 정의하는 “계약”의 역할을 합니다.
이는 클래스가 인터페이스에 정의된 모든 메소드를 반드시 구현하도록 강제합니다.
인터페이스는 메소드의 실제 구현을 포함하지 않고, 메소드의 시그니처(이름, 매개변수 리스트, 반환 유형)만을 정의합니다.
인터페이스를 사용하는 주된 목적은 다음과 같습니다.
1. 추상화(Abstraction) : 인터페이스를 통해 구현의 세부 사항을 숨기고, 사용자에게 필요한 기능만을 제공할 수 있습니다.
이렇게 함으로써 코드의 복잡성을 줄이고, 유지 관리가 쉬워집니다.
2. 다형성(Polymorphism) : 다양한 클래스들이 동일한 인터페이스를 구현함으로써, 다양한 타입의 객체를 동일한 방식으로 처리할 수 있습니다.
이는 코드의 유연성과 재사용성을 높입니다.
3. 결합도 감소(Decoupling) : 인터페이스를 통해 서로 다른 코드 부분 간의 결합도를 낮추어, 각 부분을 독립적으로 개발하고 테스트할 수 있게 합니다.
👉 인터페이스 예시
예를 들어, List 인터페이스는 add ,remove, get, size 등의 메소드를 정의하며, 이 인터페이스를 구현하는 ArrayList, LinkedList 등의 클래스는 이 메소드들을 실제로 구현해야 합니다.
이를 통해 사용자는 구체적인 리스트의 구현 방법을 몰라도 이 인터페이스를 통해 리스트를 사용할 수 있습니다.
이런 방식으로 인터페이스는 참조형 자료형 중 하나로서, 객체의 행동을 정의하고 다양한 구현을 가능하게 합니다.
-
-
-
[Math] 기초수학 - 소개
자바 프로그래밍, 수학, 자료구조 / 알고리즘
수학은 자바 프로그래밍과 자료구조 / 알고리즘 사이의 “중간 다리 역할”을 합니다.
자바 프로그래밍, 수학, 자료구조 그리고 알고리즘은 컴퓨터 과학과 소프트웨어 개발의 중요한 구성 요소들이며 서로 긴밀하게 연결되어 있습니다.
각각의 분야가 어떻게 상호작용하는지 살펴봅시다.
1. 수학과 프로그래밍.
🙋♂️ 수학과 자바 프로그래밍 사이의 상관관계는 매우 밀접하며, 효과적인 프로그래밍 기술과 문제 해결 능력을 개발하는 데 중요한 역할을 합니다. 수학은 프로그래밍의 논리적 사고, 구조적 접근, 그리고 복잡한 문제의 해결에 기초를 제공합니다.
다음은 수학이 자바 프로그래밍과 어떻게 연결되는지에 대한 몇 가지 주요 포인트입니다.
1. 논리적 사고와 알고리즘 개발 : 수학은 논리적이고 체계적인 사고를 필요로 합니다. 자바 프로그래밍에서도 마찬가지로, 문제를 분석하고 효과적인 알고리즘을 설계하는 데 이러한 사고방식이 요구됩니다.
예를 들어, 조건문, 반복문, 함수 등의 기본적인 프로그래밍 구조는 수학적 조작과 비슷한 추론을 통해 최적화될 수 있습니다.
2. 복잡도 분석 : 프로그램의 성능을 평가하고 최적화하기 위해 수학적인 복잡도 분석이 사용됩니다.
빅 오 표기법 같은 수학적 도구는 알고리즘의 실행 시간과 필요한 메모리 곤간을 예측하는 데 도움을 줍니다.
이는 효율적인 자바 프로그램을 작성하는 데 필수적인 요소입니다.
3. 문제 해결 : 수학적 모델링과 이론은 자바 프로그래밍에서 복잡한 문제를 단순화하고 구조화하는데 사용됩니다.
예를 들어, 경로 찾기, 스케줄링 문제, 최적화 문제 등을 해결할 때 수학적 기법이 프로그래밍 로직의 기반을 형성합니다.
4. 데이터 구조 : 수학적 개념, 특히 집합론은 자바에서 사용되는 다양한 데이터 구조의 이해를 돕습니다.
배열, 리스트, 스택, 큐, 트리, 그래프 등의 자료구조는 모두 수학적 원리에 기반을 두고 있으며, 이를 이해하고 활용하는 것은 효율적인 프로그래밍에 직결됩니다.
5. 인공 지능과 머신 러닝 : 자바 프로그래밍에서 머신 러닝과 인공 지능 애플리케이션을 개발할 때, 선형대수학, 확률론, 통계학 등의 수학적 분야가 필수적입니다.
이러한 수학적 지식은 데이터를 분석하고, 알고리즘을 구현하는 데 필요합니다.
6. 암호화와 보안 : 암호화 알고리즘과 보안 기술의 개발에도 수학이 깊숙이 관련되어 있습니다.
예를 들어, 공개 키 암호화 같은 기술은 수학적 난제에 기반을 두고 있으며, 이는 자바 보안 기능의 핵심 부분입니다.
👉 이처럼 수학은 자바 프로그래밍에서 논리적 구조, 효율성, 그리고 문제 해결 능력을 개발하는 데 필수적인 도구입니다.
👉 수학적 사고방식은 효과적인 소프트웨어 개발을 위한 기초적인 스킬로, 프로그래머가 보다 복잡한 문제에 접근하고 해결하는 데 큰 도움을 줍니다.
2. 수학과 자료구조.
🙋♂️ 수학과 자료구조 간의 상관관계는 컴퓨터 과학의 깊은 수학적 기반을 통해 잘 드러납니다.
다음은 수학과 자료구조 간의 몇 가지 중요한 상호작용을 설명합니다.
1. 이론적 기반 제공 : 수학은 자료구조를 이해하고 분석하는 데 필요한 이론적 지반을 제공합니다.
예를 들어, 집합 이론은 자료구조 설계의 기본이 되며, 다양한 자료구조들이 데이터의 집합을 어떻게 조직화하고 관리하는지 이해하는 데 도움을 줍니다.
2. 복잡도 분석 : 자료구조의 효율성을 평가하기 위해 수학적 도구가 필요합니다.
빅 오 표기법(O notation)은 알고리즘과 자료구조의 시간 복잡도와 공간 복잡도를 표현하는 데 사용되며, 이는 수학적 함수로 표현됩니다.
이를 통해 개발자들은 자료구조의 성능을 정량적으로 비교하고 분석할 수 있습니다.
3. 그래프 이론 : 그래프 이론은 네트워크, 소셜 미디어, 경로 탐색 등 다양한 문제를 모델링하는 데 사용되는 자료구조인 그래프의 분석과 최적화에 사용됩니다.
이는 컴퓨터 네트워크, 최단 경로 문제, 최소 스패닝 트리 등의 문제 해결에 필수적입니다.
4. 논리와 증명 : 수학적 논리와 증명 기법은 자료구조의 올바른 작동을 보장하는 데 중요합니다.
예를 들어, 자료구조릐 구현을 검증하거나, 특정 알고리즘이 주어진 자료구조에서 올바르게 작동함을 증명할 때 사용됩니다.
또한, 재귀적 자료구조와 알고리의 증명에도 수학적 귀납법이 활용됩니다.
5. 최적화 문제 : 다양한 자료구조는 종종 최적화 문제를 해결하는 데 사용됩니다.
예를 들어, 트리 구조를 사용하여 데이터베이스 쿼리의 응답 시간을 최소화하거나, 해시 테이블을 사용하여 데이터 접근 시간을 최적화할 수 있습니다.
이러한 최적화 문제는 수학적 모델링과 알고리즘을 통해 접근됩니다.
6. 확률론과 통계 : 일부 자료구조는 확률론과 통계적 방법에 기반을 둔 설계가 필요합니다.
예를 들어, 블룸 필터와 같은 확률적 자료구조는 데이터의 존재를 빠르게 검사하면서 오차를 허용하는 구조입니다.
이러한 자료구조는 확률론적 모델을 사용하여 성능과 오차 확률을 예측합니다.
👉 이처럼 수학은 자료구조의 설계, 분석, 최적화 및 검증에 깊이 관여하여, 효율적인 소프트웨어 시스템과 알고리즘의 개발을 가능하게 합니다.
👉 수학적 사고는 컴퓨터 과학에서 중요한 문제 해결 도구로 활용되며, 이를 통해 보다 정교하고 효율적인 프로그래밍이 이루어집니다.
3. 수학과 알고리즘.
🙋♂️ 수학과 알고리즘 사이의 상관관계는 컴퓨터 과학에서 매우 깊고 중요합니다. 수학은 알고리즘의 기초를 제공하며, 효율적인 알고리즘 설계와 분석을 위해 필수적인 도구와 개념들을 제공합니다.
다음은 수학이 알고리즘과 어떻게 연결되는지에 대한 몇 가지 주요 사례입니다.
1. 알고리즘 분석 : 알고리즘의 효율성을 평가하기 위해 수학적 도구가 필수적입니다. 시간 복잡도와 공간 복잡도를 정량화하기 위해 빅 오 표기법(O-notation), 빅 세타 표기법(Θ-notation), 빅 오메가 표기법(Ω-notation)등이 사용됩니다.
이러한 복잡도 분석은 알고리즘을 선택하고 최적화하는 데 중요한 기준을 제공합니다.
2. 최적화 : 수학적 최적화 기법은 알고리즘에서 특정 목표(예: 최소 비용, 최대 이익, 최소 시간)를 달성하기 위해 사용됩니다.
선형 프로그래밍, 정수 프로그래밍. 동적 프로그래밍 등의 방법이 알고리즘 설계에 자주 사용됩니다.
이러한 방법들은 복잡한 문제를 더 효율적으로 해결할 수 있도록 도와줍니다.
3. 그래프 이론 : 그래프 이론은 네트워크 경로, 소셜 네트워크, 웹 페이지 링크 구조와 같은 다양한 알고리즘 문제를 표현하고 해결하는 데 사용됩니다.
최단 경로 찾기(다익스트라 알고리즘, 벨만-포드 알고리즘), 최소 신장 트리(프림 알고리즘, 크루스칼 알고리즘)와 같은 알고리즘은 모두 그래프 이론을 기반으로 합니다.
4. 확률론과 통계 : 확률론은 불확실성 하에서 문제 해결과 의사 결정에 중요한 역할을 합니다.
예를 들어, 랜덤화 알고리즘, 몬테 카를로 방법, 라스베가스 알고리즘과 같은 확률적 알고리즘은 이론적 분석과 함께 실제 응용에서도 중요합니다.
또한, 기계 학습 알고리즘의 기초로서 확률 모델을 사용합니다.
5. 논리학 : 수학적 논리는 알고리즘의 정확성을 증명하는 데 필요합니다.
증명 기법, 예를 들어 귀납법과 수학적 귀납법은 알고리즘의 정확성을 보장하며, 특정 조건에서의 알고리즘의 동작을 증명하는 데 사용됩니다.
6. 기하학과 알고리즘 : 기하학은 컴퓨터 그래픽, 로봇 공학, 컴퓨터 비전 들에서 중요한 알고리즘을 제공합니다.
예를 즐어, 충돌 감지, 물체 인식, 경로 계획 등에 사용되는 계산 기하학은 복잡한 기하학적 구조를 효율적으로 계산하는 알고리즘을 개발하는 데 필요합니다.
👉 이와 같이, 수학은 알고리즘을 설계하고 분석하는 데 필수적인 도구이며, 효율적이고 신뢰할 수 있는 소프트웨어 시스템을 개발하는 데 중요한 역할을 합니다.
👉 수학적 사고는 알고리즘의 성능을 최적화하고 문제 해결과정을 체계화 하는 데 큰 도움이 됩니다.
-
☕️[Java] 자바 - 변수와 자료형(1)
변수와 자료형(1)
🙋♂️ 변수 이름 규칙
자바 프로그래밍에서 변수를 명명할 때 따라야 할 몇 가지 기본적인 규칙과 관례가 있습니다.
이러한 규칙을 준수하는 것은 코드의 가독성과 유지보수성을 높이는 데 중요합니다.
다음은 자바에서 변수 이름을 지정할 때 고려해야 할 주요 규칙들 입니다.
1️⃣ 기본 규칙.
**문자와 숫자, (Underscore), $ 사용가능 :** 변수 이름은 문자(letter) 나 밑줄(, Underscore) 또는 $(달러 기호)로 시작할 수 있습니다.
그러나 숫자로 시작할 수는 없습니다.
숫자로 시작할 수 없다 : 첫 글자로는 숫자로 변수 이름을 시작할 수 없습니다.
그러나 첫 글자 이후에는 숫자가 포함될 수 있습니다.
대문자와 소문자를 구분함 : 변수 이름을 명명시, 대문자와 소문자를 구분합니다.
예를 들어 int apple = 1;, int Apple = 2;, int APPLE = 3;은 모두 다른 변수로 취급됩니다.
공백을 허용하지 않음 : 변수 이름을 명명시 공백이 들어가서는 않됩니다.
예를 들어 int my friends = 7;과 같이 공백이 들어가서는 안됩니다.
특수 문자 제한 : ‘_(underscore)’와 ‘$’를 제외한 특수 문자는 변수명으로 사용할 수 없습니다.
예를 들어 ‘@’, ‘#’, ‘%’ 등은 변수 이름으로 사용할 수 없습니다.
자바 예약어 사용 금지 : int, class, static 등 자바에서 이미 의미를 갖는 예약어는 변수 이름으로 사용할 수 없습니다.
2️⃣ 표기법 및 관례(컨벤션)
카멜 케이스 : 첫 단어는 소문자로 시작하고, 이어지는 각 단어의 첫 글자는 대문자로 시작합니다.
예를 들어, firstName, totalAmount 등 입니다.
파스칼 케이스 : 각 문자의 첫 문자를 대문자로 표기합니다.
예를 들어, MyFriends, ToTalCount 등 입니다.
의미 있는 이름 : 변수 이름은 그 변수가 무엇을 의미하는지 명확하게 표현해야 합니다.
예를 들어, numberOfStudents 는 학생 수를, temperature는 온도를 나타내는 등의 명확한 이름을 사용하는 것이 좋습니다.
상수 이름 규칙 : 상수(변하지 않는 값)는 모두 대문자를 사용하며, 단어 사이는 밑줄(‘_‘)로 구분합니다.
예를 들어 MAX_HEIGHT, TOTAL_COUNT 등 입니다.
-
☕️[Java] 코테 맛보기(2) - 코테를 위한 자료구조와 알고리즘 개념 구현 방법 숙지
😋 코테 맛보기.
코딩 테스트를 준비하기 위해서 자료구조와 알고리즘은 매우 중요한 영역입니다.
이 분야들에 대한 깊이 있는 이해와 숙지는 테스트에서 성공적인 성과를 내는 데 결정적인 역할을 합니다.
다음은 코딩 테스트를 위해 필요한 자료구조와 알고리즘의 개념 및 구현 방법에 대한 가이드입니다.
1️⃣ 자료구조.
기본 자료구조 : 배열, 스택, 큐, 링크드 리스트.
이러한 자료구조들은 다양한 문제에서 데이터를 효율적으로 관리하는 기본적인 방법을 제공합니다.
고급 자료구조 : 트리(특히 이진 검색 트리), 힙, 그래프, 해시 테이블, 집합 등
이들은 보다 복잡한 데이터 관계를 다루는 데 사용되며, 특정 유형의 문제를 해결하는 데 특화되어 있습니다.
응용 자료구조 : 트라이, 세그먼트 트리, 유니온 파인드, 비트마스크 등.
이들은 특정 알고리즘 문제에 최적화된 솔루션을 제공합니다.
2️⃣ 알고리즘.
정렬 알고리즘 : 버블 정렬, 삽입 정렬, 선택 정렬, 퀵 정렬, 병합 정렬 등.
정렬은 많은 문제에서 데이터를 조작하는 기본적인 방법입니다.
검색 알고리즘 : 선형 검색, 이진 검색 등
데이터 내에서 특정 항목을 찾는 방법입니다.
재귀 알고리즘과 백트래킹 : 문제를 더 작은 문제로 나누어 해결하는 기법입니다.
동적 프로그래밍 : 복잡한 문제를 간단한 하위 문제로 나누어 해결하고, 그 결과를 저장하여 효율적으로 최종 결과를 도출합니다.
그래프 알고리즘 : 깊이 우선 탐색(DFS), 너비 우선 탐색(BFS), 최단 경로 문제(다익스트라, 플로이드-워셜), 최소 신장 트리(크루스칼, 프림) 등을 포함합니다.
3️⃣ 코테 준비 방법.
이론 학습 : 자료구조와 알고리즘의 이론을 철저히 학습합니다.
이론적인 이해는 효과적인 구현의 기초가 됩니다.
실습 연습 : 이론을 바탕으로 다양한 문제를 실제로 코딩해 봄으로써 실력을 키웁니다.
LeetCode, HackerRank, Codeforces, 백준, 프로그래머스 등의 플랫폼에서 문제를 풀어봅니다.
알고리즘 패턴 학습 : 자주 출제되는 문제 유형과 그에 대한 표준적인 해결 방법을 익힙니다.
시간 관리 연습 : 코딩 테스트에서는 제한된 시간 내에 문제를 해결해야 하므로, 시간 관리 능력을 향상시킬 필요가 있습니다.
📝 정리.
코테를 위한 준비 과정에서 이러한 자료구조와 알고리즘에 대한 이해와 숙련도는 문제를 정확하고 효율적으로 해결할 수 있는 능력을 직접적으로 높여 줍니다.
따라서, 이 분야에 대한 철저한 준비와 연습을 통해 자신감을 갖고 테스트에 임할 수 있습니다.
-
☕️[Java] 코테 맛보기(1) - 코테를 위한 자바 프로그래밍 언어 사용 숙련도
😋 코테 맛보기.
코딩 테스트를 위해 자바 프로그래밍 언어를 사용하려면 몇 가지 중요한 요소에 숙력도를 갖추어야 합니다.
자바는 많은 기업들이 코딩 테스트에 사용하는 언어 중 하나이며, 효과적인 문제 해결을 위해 다음과 같은 능력을 개발하는 것이 중요합니다.
효과적인 문제 해결을 위해 다음과 같은 능력을 개발하는 것이 중요합니다.
1️⃣ 기본 문법 숙지.
자바의 기본 문법과 프로그래밍 구조에 익숙해져야 합니다.
변수, 데이터 타입, 연산자, 제어문(if, for, while 등), 메소드 호출 등 기본적인 구성 요소를 이해하고 사용할 수 있어야 합니다.
2️⃣ 객체 지향 프로그래밍(OOP)이해.
자바는 객체 지향 프로그래밍 언어입니다.
클래스, 객체, 상속, 다형성, 캡슐화 등의 객체 지향 개념을 이해하고 이를 문제 해결에 적절히 적용할 수 있어야 합니다.
OOP 개념은 코드의 재사용성과 모듈성을 높여줘 효율적인 프로그래밍을 가능하게 합니다.
3️⃣ 표준 라이브러리 사용.
자바의 표준 라이브러리에는 다양한 자료구조와 알고리즘이 구현되어 있습니다.
java.util 패키지 내의 컬렉션 프레임워크(리스트, 맵, 셋 등)를 비롯해, 유용한 유틸리티 클래스들을 활용할 줄 알아야 합니다.
이러한 라이브러리들은 코딩 테스트에서 효율적인 코드 작성을 돕습니다.
4️⃣ 알고리즘과 자료구조.
다양한 알고리즘과 자료구조에 대한 이해가 중요합니다.
정렬, 탐색, 그래프 이론, 동적 프로그래밍 등의 알고리즘과 배열, 스택, 큐, 링크드 리스트, 트리 등의 자료구조에 대한 깊은 이해가 필요합니다.
이는 문제를 효과적으로 분석하고 최적의 해결책을 구현하는 데 결정적입니다.
5️⃣ 문제 해결 능력.
실제 코딩 테스트에서는 다양한 유형의 문제가 제시됩니다.
문제를 빠르게 이해하고 효과적인 해결책을 설계할 수 있는 능력이 필요합니다.
이는 실전 연습을 통해 향상시킬 수 있으며, 온라인 코딩 플랫폼에서 다양한 문제를 풀어 보는 것이 좋습니다.
6️⃣ 테스트와 디버깅
코드가 예상대로 동작하는지 검증하고, 오류를 찾아 수정할 수 있는 능력도 중요합니다.
자바에서 제공하는 디버깅 도구를 사용하여 코드를 단계별로 실행하고, 변순의 상태를 확인하며 문제를 진단할 수 있어야 합니다.
📝 정리.
코딩 테스트를 위한 자바 숙련도는 이론적 지식과 실제 적용 능력의 조합을 요구합니다.
이를 위해 개념 학습과 함께 많은 실습을 병행하는 것이 중요합니다.
시간을 정해두고 실전처럼 문제를 풀어보는 연습을 꾸준히 하면, 효과적으로 자바를 활용하여 코딩 테스트에서 좋은 성과를 낼 수 있을 것입니다.
-
☕️[Java] 자바 - 소개
자바 - 소개
🙋♂️ 1. 자바의 특징.
자바는 세계적으로 널리 사용되는 프로그래밍 언어로, 웹 개발, 모바일 애플리케이션, 대규모 시스템 구축 등 다양한 분야에 활용됩니다.
자바의 주요 특징들은 다음과 같습니다.
1️⃣ 플랫폼 독립성.
“Write Once, Run Anywhere”(WORA) : 자바 프로그램은 자바 가상 머신(JVM) 위에서 실행되기 때문에, 한 번 작성하면 어떤 플랫폼에서도 실행할 수 있습니다.
이는 자바 컴파일러가 소스 코드를 플랫폼 독립적인 바이트코드로 변환하기 때문입니다.
2️⃣ 객체 지향 프로그래밍(OOP).
자바는 객체 지향 프로그래밍 언어로, 캡슐화, 상속, 다형성 등을 완전히 지원합니다.
이는 코드 재사용, 유지 관리의 용이성 및 시스템 모듈화를 가능하게 합니다.
3️⃣ 강력한 표준 라이브러리.
자바는 방대한 표준 라이브러리를 제공하여, 네트워킹, 파일 시스템 접근, 그래픽 인터페이스 제작 등 다양한 작업을 쉽게 처리할 수 있도록 돕습니다.
4️⃣ 메모리 관리.
자동 가비지 컬렉션
자바는 사용하지 않는 객체를 자동으로 감지하고 메모리에서 제거하는 가비지 컬렉터를 내장하고 있습니다. 이는 개발자가 메모리 누수에 대해 걱정할 필요가 적어지게 해줍니다.
5️⃣ 보안.
자바는 샌드박스 환경에서 애플리케이션을 실행하여 시스템 리소스에 대한 무단 접근을 방지합니다.
또한, 클래스 로더, 바이트코드 검증기 등을 통해 애플리케이션이 안전하게 실행될 수 있도록 합니다.
6️⃣ 멀티스레딩.
자바는 내장된 멀티스레딩 기능을 지원하여, 여러 스레드가 동시에 실행되도록 하여 애플리케이션의 효율성을 높입니다.
이는 특히 네트워크 서버와 실시간 시스템에서 큰 장점입니다.
7️⃣ 로버스트와 포터빌리티.
자바 프로그램은 다른 플랫폼으로의 이동성이 뛰어나며, 높은 수준의 안정성을 제공합니다.
예외 처리 기능을 통해 오류를 쉽게 관리하고, 시스템의 안정성을 높일 수 있습니다.
📝 마무리.
자바의 이러한 특징들은 그것을 매우 유연하고, 다양한 애플리케이션 개발에 적합하게 만듭니다.
이로 인해 자바는 세계적으로 인기 있는 프로그래밍 언어 중 하나로 자리 잡게 되었습니다.
🙋♂️ 2. 자바 프로그램의 작성과 실행과정.
1️⃣ 소스 코드 작성.
개발자는 자바의 문법에 맞추어 .java 확장자 파일에 소스 코드를 작성합니다.
이 파일에는 하나 이상의 클래스가 포함되며, 각 클래스는 데이터와 메서드를 정의합니다.
2️⃣ 컴파일.
소스 코드 파일을 자바 컴파일러(javac)를 사용하여 컴파일합니다.
컴파일러는 소스 코드를 읽고, 문법 오류를 검사한 후, 바이트코드라는 중간 형태의 코드로 변환합니다.
이 바이트 코드는 .class 파일로 저장됩니다.
바이트코드는 플랫폼 독립적이기 때문에, 한 번 컴파일된 .class 파일은 다양한 운영 체제에서 실행될 수 있습니다.
3️⃣ 로딩.
자바 가상 머신(JVM)은 .class 파일을 로드합니다.
클래스 로더(component of JVM)가 이 작업을 수행하며, 필요한 클래스 파일들을 메모리에 로드합니다.
4️⃣ 링킹.
로드된 클래스 파일들은 링킹 과정을 거칩니다. 링킹은 검증, 준비, 그리고(선택적으로) 해석 단계를 포함합니다.
검증 : 로드된 바이트코드가 올바르게 포맷되었는지, 안전한지 검사합니다.
준비 : 클래스 변수와 기본값을 위한 메모리를 할당합니다.
해석 : 심볼릭 메모리 참조를 직접 참조로 변환합니다(선택적).
5️⃣ 초기화.
클래스 초기화 단계에서 정적 변수들에 대한 초기화가 수행되며, 정적 블록이 실행됩니다.
6️⃣ 실행.
프로그램 실행 동안 JVM 내부에서 가비지 컬렉터가 사용되지 않는 객체를 자동으로 감지하고, 할당된 메모리를 해제하여 메모리를 관리합니다.
📝 마무리.
자바의 이러한 실행 과정은 코드의 플랫폼 독립성을 보장하고, 안정적이며 보안적인 실행 환경을 제공합니다.
이 모든 과정은 개발자로부터 대부분 숨겨져 있으며, 개발자는 주로 소스 코드 작성과 일부 디버깅에 집중할 수 있습니다.
-
☕️[Java] 자바란?
자바란?
자바 언어 특징.
1. 타 언어에 비해 배우기 쉽습니다.
2. 플랫폼에 독립적입니다.
- 자바 언어가 플랫폼에 독립적인 이유는 그 설계 철학과 메커니즘에 근거합니다.
- 자바는 "한 번 작성하면, 어디서든 실행된다(Write Once, Run Anywhere, WORA)" 라는 철학을 실현하기 위해 개발되었습니다.
- 이를 가능하게 하는 핵심 요소는 자바 가상 머신(Java Virtual Machine, JVM)과 자바 바이트코드의 도입입니다.
자바의 플랫폼 독립성의 주요 요인
1. 자바 가상 머선(JVM)
JVM은 자바 바이트 코드를 실행할 수 있는 런타임 환경을 제공합니다. 자바 프로그램이 컴파일되면, 플랫폼에 독립적인 바이트코드로 변환됩니다.
이 바이트코드는 어떤 특정 하드웨어나 운영 체제의 기계어 코드가 아닌, JVM이 이해할 수 있는 중간 형태의 코드입니다.
JVM은 바이트코드를 받아 각 플랫폼에 맞는 기계어 코드로 변환하고 실행합니다.
따라서, 자바 애플리케이션은 다양한 운영 체제에서 JVM만 설치되어 있으면 실행될 수 있습니다.
2. 컴파일과 실행의 분리
자바 프로그램은 소스 코드(.java 파일)에서 바이트코드(.class 파일)로 컴파일되는 과정과, 실행 시 바이트 코드가 실제로 실행되는 과정으로 나누어집니다. 이 두 단계의 분리는 프로그램을 한 번 컴파일하면, 그 컴파일된 코드가 다양한 환경의 JVM에서 실행될 수 있게 합니다.
3. 표준화된 API
자바는 풍부하고 표준화된 API를 제공합니다. 이 API들은 플랫폼에 관계없이 일관된 방식으로 작동하므로, 개발자는 운영 체제의 특징을 신경 쓰지 않고도 애플리케이션을 개발할 수 있습니다. 예를 들어, 파일 시스템 접근, 네트워크 프로그래밍 등의 기능은 모든 플랫폼에서 동일한 자바 코드로 작동합니다.
4. 언어와 라이브러리의 독립성
자바 언어와 표준 라이브러리는 플랫폼에 특화된 구현으로부터 독립적입니다.
즉, 자바의 표준 라이브러리 구현은 다양한 하드웨어와 운영 체제에서 동일하게 작동하도록 설계되었습니다.
3. 객체지향 프로그래밍입니다.
객체지향 프로그래밍?
자바에서의 객체지향 프로그래밍(Object-Oriented Programming, OOP)은 소프트웨어를 설계하고 구현할 때 객체라는 개념을 중심으로 프로그래밍하는 방식을 말합니다.
객체지향 프로그래밍은 코드의 재사용성, 확장성 및 관리 용이성을 높이는 데 도움이 됩니다.
자바는 객체지향 언어의 특징을 강하게 반영하고 있으며, 다음과 같은 기본 원칙에 따라 프로그래밍 됩니다.
1. 캡슐화(Encapsulation)
객체의 데이터(속성)와 그 데이터를 조작하는 메소드를 하나의 단위로 묶는 것을 말합니다.
캡슐화를 사용하면 객체의 세부 구현 내용을 외부에서 알 필요 없이 객체가 제공하는 기능만을 사용할 수 있으며, 이는 코드의 유지보수를 용이하게 합니다.
2. 상속(Inheritance)
한 클래스가 다른 클래스의 특성을 상속 받아 사용할 수 있게 하는 것입니다.
이를 통해(상속을 통해) 기존 코드를 재사용하면서 확장할 수 있고, 코드의 중복을 줄이며 유지 보수가 쉬워집니다.
3. 다형성(Polymorphism)
같은 이름의 메소드가 다른 작업을 수행할 수 있도록 하여 메소드의 오버라이딩(Overriding)이나 오버로딩(Overloading)을 가능하게 합니다.
오버라이딩(Overriding) : 자식 클래스가 상속 받은 부모 클래스의 메소드를 재정의 하는 행위를 말합니다. 오버라이딩을 통해 자식 클래스는 상속 받은 메소드와 동일한 시그니처(메소드 이름, 매개변수 리스트)를 가지지만, 그 내용을 자신의 특정한 요구에 맞게 새롭게 구현할 수 있습니다. 오버라이딩된 메소드는 실행 시 다형성을 활용하여 해당 객체의 실제 타입에 따라 적절한 메소드가 호출됩니다.
예시 코드
java
class Animal {
void display() {
System.out.println("This is an animal.");
}
}
class Cat extends Animal {
@Override
void display() {
System.out.println("This is a cat.")
}
}
오버로딩(Overloading) : 같은 클래스 내에서 같은 이름의 메소드를 여러 개 정의할 수 있도록 하지만, 매개변수의 타입, 개수 또는 순서가 달라야 합니다. 이를 통해 메소드에 다양한 입력 파라미터를 제공할 수 있으며, 프로그래머가 같은 동작을 하는 메소드에 대해 다양한 옵션을 제공할 수 있습니다. 오버로딩은 컴파일 시간에 결정되며, 메소드 호출 시 전달된 매개변수에 따라 적절한 메소드가 선택됩니다.
예시 코드
class Display {
void show(int a) {
System.out.println("Number: " + a);
}
void show(String a) {
System.out.println("String: " + a);
}
void show(int a, int b) {
System.out.println("Two numbers: " + a + ", " + b);
}
}
이처럼 오버라이딩과 오버로딩은 자바 프로그래밍에서 메소드의 기능을 확장하거나 변경할 때 유용하게 쓰이는 기법입니다.
오버라이딩은 주로 다형성을 활용한 동적 바인딩을 목적으로 하며, 오버로딩은 같은 이름의 메소드에 여러 입력 형태를 제공하기 위해 사용됩니다.
4. 추상화(Abstraction)
복잡한 실제 상황을 단순화하는 과정에서 중요한 특징만을 추출하여 프로그램 코드에 반영하는 것을 의미합니다.
추상 클래스와 인터페이스를 통해 구현될 수 있습니다.
이러한 원칙들은 자바를 사용하여 복잡한 시스템을 개발할 때 코드의 모듈화를 가능하게 하고, 이로 인해 대규모 소프트웨어 개발과 프로젝트 관리가 용이해집니다.
코드의 모듈화(Modularization): 큰 프로그램을 작은 세부 모듈로 나누는 프로세스를 의미합니다. 이러한 모듈은 각각 독립적인 기능을 수행하며, 전체 시스템의 한 부분으로 기능합니다. 모듈화의 주요 목적은 프로그램의 관리를 용이하게 하고, 개발을 효율적으로 만들며, 코드의 재사용성을 높이는 것입니다.
모듈화의 주요 이점은 다음과 같습니다.
1. 유지보수성
모듈화된 코드는 각 모듈이 분리되어 있기 때문에, 하나의 모듈에서 발생한 문제가 다른 모듈에 미치는 영향을 최소화할 수 있습니다. 따라서 개별 모듈을 독립적으로 수정, 업데이트, 테스트할 수 있어 전체 코드베이스의 유지보수가 더 쉬워 집니다.
모듈(Module): 소프트웨어 설계에서 사용되는 기본 개념 중 하나로, 관련된 기능들을 논리적으로 그룹화하고 독립적으로 사용할 수 있는 코드의 단위를 의미합니다. 모듈은 프로그램의 특정 기능을 담당하며, 독립적인 개발, 테스트, 재사용이 가능하도록 설계됩니다. 모듈화된 코드는 대체로 명확하고 관리하기 쉬운 구조를 갖습니다.
모듈의 특징으로는 다음과 같습니다.
1. 독립성
모듈은 가능한 한 다른 모듈과 독립적으로 동작할 수 있어야 하며, 이를 통해 시스템의 복잡성을 줄이고, 각 모듈의 재사용성을 높일 수 있습니다.
2. 캡슐화
모듈은 자신의 구현 세부사항을 숨기고, 필요한 기능만을 외부에 제공하는 인터페이스를 통해 상호작용합니다. 이로 인해 모듈 간의 상호 의존성이 줄어들고, 변경 관리가 용이해집니다.
3. 인터페이스
모듈은 정의된 인터페이스를 통해 외부와 통신합니다. 인터페이스는 모듈이 제공하는 기능과 해당 기능을 어떻게 접근할 수 있는지를 명시합니다.
모듈의 예로는 다음으로 들 수 있습니다.
라이브러리
특정 기능을 제공하는 함수나 데이터 구조를 모아 놓은 코드 집합. 예를 들어, 수학 연산을 위한 수학 라이브러리, 데이터베이스 작업을 위한 데이터베이스 접근 라이브러리 등이 있습니다.
클래스
객체지향 프로그래밍에서 클래스는 속성(데이터)과 메소드(함수)를 캡슐화하여 모듈을 형성합니다. 클래스는 독립적으로 사용될 수 있으며, 다른 클래스와 상호작용할 수 있습니다.
패키지
관련된 여러 클래스나 모듈을 하나의 더 큰 단위로 그룹화한 것 입니다. 예를 들어, Java에서는 java.util 패키지가 여러 유틸리티 클래스와 인터페이스를 제공합니다.
모듈은 개발 과정을 체계화하고, 코드의 재사용성을 증가시키며, 유지 관리를 용이하게 하는 중요한 역할을 합니다.
모듈은 크기가 클 수도 있고 작을 수도 있으며, 프로젝트의 요구와 설계에 따라 그 범위와 기능이 결정됩니다.
2. 재사용성
잘 설계된 모듈은 다른 프로그램에서도 재사용할 수 있습니다. 이는 소프트웨어 개발 시간과 비용을 줄이는 데 도움이 되며, 일관된 기능을 여러 프로젝트에 걸쳐 사용할 수 있습니다.
3. 확장성
모듈화는 시스템의 확장성을 향상시킵니다. 새로운 기능이 필요할 때 기존 모듈을 수정하거나 새로운 모듈을 추가하기가 더 쉬워집니다. 이는 시스템의 유연성을 증가시키고, 변화하는 요구사항에 더 잘 대응할 수 있게 합니다.
4. 가독성
작은 모듈로 나뉘어진 코드는 각각의 모듈이 명확한 기능을 수행하기 때문에, 전체 코드의 구조를 이해하기가 더 쉽습니다. 개발자가 프로그램의 특정 부분만을 이해하고도 효과적으로 작업할 수 있습니다.
5. 팀 협업 향상
모듈화는 여러 개발자가 동시에 다른 모듈에서 작업할 수 있게 함으로써 팀 작업을 용이하게 합니다. 각 팀원이 특정 모듈에 집중할 수 있으며, 전체 프로젝트에 대한 의존성을 줄이면서 협업을 효율적으로 진행할 수 있습니다.
이처럼 코드의 모듈화는 소프트웨어 개발 과정에서 중요한 역할을 하며, 특히 대규모 프로젝트나 복잡한 시스템 개발에 있어 필수적인 접근 방식입니다.
4. Garbage Collector로 사용되지 않는 메모리를 자동적으로 정리해줍니다.
Garbage Collector(GC): 프로그램이 동적으로 할당한 메모리 영역 중에서 더 이상 사용하지 않는 부분을 자동으로 찾아서 해제하는 시스템을 말합니다. 이 과정을 통해 프로그램에서 발생할 수 있는 메모리 누수를 방지하고, 사용 가능한 메모리 리소스를 최적화합니다.
프로그램이 동적으로 할당한 메모리 영역 : 프로그램 실행 중에 필요에 따라 할당되고 해제되는 메모리를 말합니다. 이는 프로그램의 런타임 중에 사용자의 요구나 데이터의 양에 따라 변화하는 메모리 요구 사항을 수용하기 위해 사용됩니다. 동적 메모리 할당은 프로그램이 시작할 때 필요한 메모리 양을 미리 알 수 없는 경우나, 실행 도중에 메모리 사용량이 변할 때 유용합니다.
동적 메모리 할당의 특징은 아래와 같습니다.
1. 유연성 : 동적 메모리 할당은 프로그램 실행 중에 필요한 메모리 크기를 조정할 수 있게 해줍니다. 이로 인해 프로그램은 사용자의 입력, 파일 크기, 또는 다른 실행 시 요소들에 따라 메모리 사용을 최적화할 수 있습니다.
2. 효율성 : 필요할 때만 메모리를 할당하고, 더 이상 사용하지 않는 메모리를 해제함으로써 시스템 리소스를 보다 효율적으로 사용할 수 있습니다.
3. 메모리 관리 : 동적 메모리는 일반적으로 힙(Heap) 영역에서 관리됩니다. 힙은 프로그램의 데이터 영역 중 하나로, 동적으로 할당되는 객체와 데이터에 사용됩니다. 힙 영역의 크기는 프로그램 실행 도중에 확장되거나 축소될 수 있습니다.
동적 메모리 할당의 예는 다음과 같습니다.
자바에서는 new 키워드를 사용하여 객체를 생성할 때 동적 메모리 할당이 일어납니다. 예를 들어, new ArrayList() 를 호출하면, 자바 런타입은 필요한 메모리를 힙에서 할당하여 ArrayList 객체를 저장합니다.
객체 사용이 끝나면 자바의 GC가 더 이상 참조되지 않는 객체가 사용하던 메모리를 자동으로 해제합니다.
동적 메모리 할당은 프로그램이 더 유연하고 효율적으로 동작하도록 돕지만, 관리가 제대로 이루어지지 않을 경우 메모리 누수나 성능 저하 같은 문제를 초래할 수 있습니다. 따라서 프로그래머는 동적 메모리 관리를 신중하게 수행해야 합니다.
GC의 주요 기능은 다음과 같습니다.
1. 메모리 관리 자동화 : 프로그래머가 메모리 할당 및 해제를 직접 관리하는 대신 자바 런타입이 이를 자동으로 처리합니다. 이로 인해 개발자는 메모리 관리에 신경 쓰지 않고, 애플리케이션 로직 개발에 더 집중할 수 있습니다.
2. 메모리 누수 방지 : GC는 참조되지 않는 객체들을 정기적으로 청소하여 메모리 누수를 방지합니다. 객체가 더 이상 필요 없을 때 자동으로 메모리에서 제거됩니다.
3. 효율적인 메모리 사용 : 사용되지 않는 객체들을 정리함으로써 메모리를 효율적으로 사용하고, 애플리케이션의 성능을 유지할 수 있도록 도와줍니다.
GC의 작동 원리는 다음과 같습니다.
GC은 크게 두 단계로 진행됩니다.
1. 객체 탐지 : GC는 더 이상 어떤 객체에도 참조되지 않는 객체들을 탐지합니다. 이러한 객체들은 프로그램에서 더 이상 사용되지 않는 것으로 간주됩니다.
2. 메모리 회수 : 탐지된 객체들이 차지하고 있는 메모리를 해제합니다. 이 메모리는 다시 사용 가능한 상태가 되어, 새로운 객체를 위해 재할당될 수 있습니다.
GC 알고리즘
자바는 다양한 GC 알고리즘을 제공합니다. 대표적인 몇 가지는 다음과 같습니다.
Mark-and-Sweep : 사용 중인 객체를 “표시(mark)”하고, 표시되지 않은 객체를 “쓸어내는(sweep)” 방식입니다.
Generational GC : 객체를 세대별로 분류하여, 생성된지 얼마 되지 않은 객체들(Young Generation)과 오래된 객체들(Old Generation)을 다르게 관리합니다. 이 방식은 대부분의 객체가 생성 후 짧은 시간 내에 소멸된다는 관찰에 기반합니다.
Compacting : 사용 중인 객체들을 메모리의 한쪽으로 몰아넣어(Compact), 메모리의 연속성을 높이고, 메모리 단편화를 방지합니다.
GC은 메모리 관리를 자동화하지만, 때로는 성능 저하를 일으킬 수 있습니다. 특히 GC가 실행되는 동안에는 프로그램의 다른 모든 작업이 일시적으로 중단(Stopping the world)될 수 있기 때문에, GC 동작 방식과 설정을 잘 이해하고 조절하는 것이 중요합니다.
JVM(Java Virtual Machine)
JVM은 자바 애플리케이션을 실행하기 위한 가상 머신으로, 자바 바이트코드를 로컬 기계 코드로 변환하여 실행하는 역할을 합니다.
자바 바이트코드(Java Bytecode) : 자바 소스 코드가 컴파일된 후의 중간 형태입니다.
자바 소스 파일(.java 파일)을 자바 컴파일러가 컴파일하면, 결과적으로 생성되는 것이 .class 파일로 저장되는 자바 바이트코드입니다.
이 바이트코드는 기계어 코드는 아니지만, CPU가 직접 실행할 수는 없고, JVM이 이해하고 실행할 수 있는 명령어 세트로 구성되어 있습니다.
바이트코드는 플랫폼에 독립적이기 때문에, 한 번 컴파일된 자바 프로램은 어떤 JVM이 설치된 시스템에서든 실행할 수 있습니다.
이는 자바의 “한 번 작성하면, 어디서든 실행된다”라는 이점을 제공합니다.
로컬 기계 코드(Local Machine Code) : 로컬 기계 코드는 특정 하드웨어 플랫폼의 CPU가 직접 이해하고 실행할 수 있는 명령어 코드입니다.
이 코드는 플랫폼에 종속적이며, 다양한 운영 체제와 하드웨어 아키텍처는 각각의 기계어 코드를 가지고 있습니다.
자바 바이트코드는 JVM을 통해 실행될 때, 두 가지 방법 중 하나로 실행될 수 있습니다.
1. 인터프리터 : JVM은 바이트코드를 한 줄씩 읽고, 각 명령을 로컬 기계 코드로 변환하면서 실행합니다. 이 방법은 간단하지만, 실행 속도가 느릴 수 있습니다.
2. JIT 컴파일러(Just-In-Time Compiler) : 이 방식에서는 JVM이 바이트코드 전체 또는 핵심 부분을 분석하여, 실행 전에 전체 코드를 로컬 기계 코드로 한번에 변환합니다. 이렇게 하면 프로그램의 실행 속도가 크게 향상됩니다.
결국, 자바 바이트코드는 플랫폼 독립적인 중간 코드로서의 역할을 하며, 로컬 기계 코드는 실제 하드웨어에서 실행되기 위한 최종적인 코드 형태입니다. 이 두 코드의 변환과 실행은 JVM 내에서 처리되며, 사용자는 이 과정을 명시적으로 관리할 필요가 없습니다. 이것이 자바가 제공하는 큰 이점 중 하나입니다.
JVM은 자바의 “한 번 작성하면, 어디서든 실행된다(Write Once, Run Anywhere, WORA)” 라는 철학을 가능하게 하는 중요한 구성 요소입니다.
JVM 덕분에 자바 애플리케이션은 운영 체제나 하드웨어 플랫폼에 구애받지 않고 동일하게 실행될 수 있습니다.
JVM의 주요 기능.
1. 플랫폼 독립성 : 자바 프로그램은 JVM 위에서 실행되므로, JVM이 설치되어 있는 모든 운영 체제에서 같은 자바 프로그램을 실행할 수 있습니다.
이는 JVM이 플랫폼에 특화된 코드로 바이트 코드를 변환하기 때문입니다.
2. 메모리 관리 : JVM은 자동 메모리 관리 기능을 제공합니다. 이는 GC를 통해 메모리 할당과 해제를 관리하여, 프로그래머가 메모리 누수 없이 효율적인 메모리 사용을 할 수 있도록 돕습니다.
3. 보안 : 자바 바이트코드는 JVM에 의해 검증되며 실행되기 전에 다양한 검사를 통해 안전성이 확보됩니다.
이는 악의적인 코드 실행과 시스템 오류를 방지하는 데 도움이 됩니다.
4. 실행 환경 : JVM은 자바 애플리케이션에 필요한 실행 환경을 제공합니다.
이 환경은 클래스 로더, 바이트코드 실행 엔진, 쓰레드 관리 등을 포함합니다.
JVM의 구성 요소.
1. 클래스 로더(Class Loader) : 클래스 파일들을 읽고 바이트코드를 JVM 메모리에 로드하는 역할을 합니다.
2. 실행 엔진(Excution Engine) : 로드된 클래스 파일의 바이트코드를 실행합니다. 이 엔진은 바이트코드를 해것하거나 필요에 따라 JTI(Just-In-Time) 컴파일러를 사용하여 바이트코드를 직접 기계 코드로 변환하여 실행 속도를 높일 수 있습니다.
3. 가비지 컬렉터(Garbage Collector) : JVM이 사용하지 않는 메모리 자원을 자동으로 회수합니다.
4. 메모리(Runtime Data Area) : JVM은 프로그램 실행을 위해 필요한 다양한 메모리 영역을 관리합니다. 이는 힙(Heap), 스택(Stack), 메소드 영역(Method Area), 프로그램 카운터(Program Counter) 등이 포함됩니다.
-
[AnD] 두 수의 합.
문제 설명 🤓
0 이상의 두 정수가 문자열 a, b로 주어질 때, a + b의 값을 문자열로 return 하는 solution 함수를 작성해 주세요.
솔루션 📝
import java.math.BigInteger;
class Solution {
public String solution(String a, String b) {
String answer = "";
BigInteger bigNumberA = new BigInteger(a);
BigInteger bigNumberB = new BigInteger(b);
answer = bigNumberA.add(bigNumberB).toString();
return answer;
}
}
트러블슈팅 🏀
1. NumberFormatException 에러(1).
입출력의 예시 중 가장 긴 입력 예시인 a : “18446744073709551615”, b : “305793246910280479981” 에서 에러가 발생 했습니다.
1️⃣ 콘솔에 나타난 에러 메시지
콘솔에 나타난 에러 메시지는 아래와 같았습니다.
Exception in thread "main" java.lang.NumberFormatException: For input string: "18446744073709551615"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:662)
at java.base/java.lang.Integer.valueOf(Integer.java:989)
at programmers.test1.Solution.solution(Solution.java:8)
at programmers.test1.SolutionMain.main(SolutionMain.java:7)
Process finished with exit code 1
2️⃣ 본격적인 트러블슈팅
이 메시지를 하나씩 해석하고 트러블슈팅을 이어갔습니다.
먼저 이 오류 메시지는 NumberFormatException 이 발생했다는 것을 나타냅니다.
특히 “For input string: “18446744073709551615”는 Java에서 정수로 변환하려는 문자열이 정부 범위를 벗어났음을 의미합니다.
Java의 Integer.parseInt() 메소드는 문자열을 정수(Int)로 변환할 때 사용됩니다.
그러나 Int 자료형은 -2,147,483,648,648 부터 2,147,483,648,647까지의 값을 저정할 수 있습니다.
제공된 문자열 “18446744073709551615”는 이 범위를 훨씬 초과합니다.
3️⃣ 해결 방법
이 문제를 해결하려면 다음과 같은 방법을 고려할 수 있습니다.
1. 타입 변경
int 대신 long 타입을 사용하거나, 이보다 더 큰 범위가 필요하다면, BigInteger 클래스를 사용할 수 있습니다.
long 의 범위는 -9,223,372,036,854,775,808부터 9,223,372,036,854,775,807 까지입니다.
2. 입력 검증
입력 값이 정수 타입으로 변환 가능한지, 그리고 해당 타입의 범위 내에 있는지 검증하는 로직을 추가합니다.
코드를 수정할 때는 적절한 데이터 타입을 사용하도록 주의해야 합니다.
예를 들어, long 으로 변경하려면 Long.parseLong() 을 사용할 수 있습니다.
2. NumberFormatException 에러(2)
이번에는 위의 트러블슈팅을 활용하여 코드를 만든 결과 NumberFormatException 에러를 다시 발생 시킨 케이스 입니다.
이번에는 Long.parseLong() 메소드를 사용하면서 발생했습니다.
문자열 “18446744073709551615”는 이번에도 범위를 벗어난 값으로 처리되었습니다.
long 자료형의 최대값은 9,223,372,036,854,775,807dlqslek.
제공된 값 “18446744073709551615”는 이 최대값을 초과합니다.
따라서, long 으로도 처리할 수 없으며, Java에서 이러한 큰 숫자를 다루려면 BigInteger 클래스를 사용해야 합니다.
BigInteger 는 사실상 제한 없는 정밀도의 정수를 다룰 수 있어 이와 같은 큰 숫자를 취급할 때 유용합니다.
BigInteger를 사용하는 예시 코드.
import java.math.BigInteger;
public class Solution {
public void solution(String input) {
BigInteger bigNumber = new BigInteger(input);
// bigNumber를 사용한 다른 로직
}
}
public class SolutionMain {
public static void main(String[] args) {
new Solution().solution("18446744073709551615");
}
}
이 코드는 BigInteger 를 사용하여 입력된 숫자를 처리하고, 필요한 로직을 수행할 수 있도록 구성되어 있습니다.
3. BigInteger 클래스를 사용하여 두 큰 정수를 더하는 방법.
두 문자열을 받아 큰 범위의 문자열을 BigInteger 클래스를 사용하여 받아오고 변환하는 데 까지는 성공하였으나 입력된 두 개의 큰 범위 값의 BigInteger 를 어떻게 합쳐야 할지를 몰라 검색해 봤습니다.
1️⃣ Java에서 BigInteger 클래스를 사용하여 두 큰 정수를 더하는 방법
BigInteger 클래스는 불변(immutable) 객체 이므로 두 BigInteger 인스턴스를 더할 때, 새로운 BigInteger 객체가 반환됩니다.
2️⃣ BigInteger 객체를 더하는 방법 예시 코드
import java.math.BigInteger;
public class Main {
public static void main(String[] args) {
// 두 큰 수를 BigInteger로 생성
BigInteger number1 = new BigInteger("12345678901234567890");
BigInteger number2 = new BigInteger("98765432109876543210");
// 두 수를 더함
BigInteger sum = number1.add(number2);
// 결과 출력
System.out.println("Sum: " + sum.toSting())
}
}
이 코드는 다음과 같은 단계를 거칩니다.
1. 두 개의 BigInteger 인스턴스 number1 과 number2 를 생성합니다.
이들은 문자열로부터 생성되며, 매우 큰 수를 나타낼 수 있습니다.
2. add 메소드를 사용하여 number1 과 number2 를 더합니다.
이 메소드는 두 수의 합을 나타내는 새로운 BigInteger 객체를 반환합니다.
3. 덧셈 결과를 출력합니다.
BigInteger 를 사용하면 정수의 범위에 제한 없이 수학적 연산을 수행할 수 있어, 매우 큰 수를 처리해야 할 때 유용합니다.
Touch background to close