-
[design-pattern] singleton 패턴
intro : singleton 패턴에 대해서 알아보자
singleton 패턴이란 ?
싱글톤 패턴(Singleton Pattern)은 객체지향 프로그래밍에서 사용되는 생성 디자인 패턴 중 하나로, 클래스의 인스턴스가 오직 하나만 존재하도록 보장하는 패턴입니다. 이 패턴은 클래스가 단 하나의 인스턴스를 공유해야 할 때 유용하며, 주로 시스템에서 전역적으로 접근 가능한 유일한 객체가 필요할 때 사용된다.
singleton 패턴은 언제 사용하는가 ?
싱글톤 패턴은 객체가 시스템 내에서 단 하나만 존재해야 할 때 사용합니다. 이 패턴은 전역적으로 공유되는 객체가 필요하거나, 여러 곳에서 동일한 인스턴스를 참조해야 할 때 유용하다.
singleton 패턴 적용 코드
싱글톤으로 사용할 클래스 정의. 예제의 상황은 시스템에 전역적으로 적용될 테마색(light,dark)에 대한 설정을 진행할 클래스와 해당 클래스를 이용하여 테마 색이 적용되는 버튼, 텍스트필드, 라벨에 대한 상황을 보겠다.
// 싱글톤으로 사용할 클래스
public class Theme {
// 자기자신의 타입의 멤버변수를 선언한다.
// static 키워드를 사용하는 이유는 전역적으로 하나만 사용하기 위함에 있음
private static Theme instance;
// 테마 칼라를 선언한다.
private String themeColor;
// 생성자가 private인 이유는, 외부에서 해당 클래스 객체를 new 연산자로
// 생성하지 못하게 하기 위함에 있다.
private Theme() {
// 기본적으로 해당 객체가 생성되었을때는 테마 색은 light로 설정한다.
this.themeColor = "light"; // Default theme
}
// 외부에서 객체를 생성하기 위해서는 해당 메서드를 이용해서
// 인스턴스를 반환받아야 한다.
public static Theme getInstance() {
// 만약 클래스의 instance 변수가 null 인경우
// 객체가 생성되어 있지 않다는 걸로 간주하고 new 연산자를 통해 내부에서 객체 생성
if (instance == null) {
instance = new Theme();
}
// 만약 instance 객체가 생성되어 있다면 주소값만 리턴
return instance;
}
// 테마 색을 얻는 메서드
public String getThemeColor() {
return themeColor;
}
// 테마 색을 설정하는 메서드
public void setThemeColor(String themeColor) {
this.themeColor = themeColor;
}
}
각 버튼과 텍스트필드 라벨에 대한 클래스를 정의하고 해당 클래스의 테마색은 싱글톤 클래스의 (Theme) 설정에 따라 display 메서드에서 themeColor가 다르게 출력된다. (색을 따로 dark로 변경하기 전까지는 light가 기본값)
// 싱글톤 클래스에서 값을 참조할 클래스
public class Button {
private String label;
public Button(String label) {
this.label = label;
}
// Theme.getInstance().getThemeColor(); 메서드가 실행 가능한 이유는
// getInstance() 메서드가 static 메서드이기 때문에 객체 생성없이도 호출이 가능하다.
// 전역적으로 하나만 존재하기에 getThemeColor 메서드 호출값은 어디서 호출하던지 동일한 값을 보장함
public void display() {
String themeColor = Theme.getInstance().getThemeColor();
System.out.println(
"Button [" + label + "] displayed in " + themeColor + " theme."
);
}
}
public class TextField {
private String text;
public TextField(String text) {
this.text = text;
}
// 위 주석과 같은 맥락으로 static 메서드 이기 때문에 Theme.getInstance().getThemeColor(); 실행이 가능
public void display() {
String themeColor = Theme.getInstance().getThemeColor();
System.out.println(
"TextField [" + text + "] displayed in " + themeColor + " theme."
);
}
}
public class Label {
private String text;
public Label(String text) {
this.text = text;
}
// 위 주석과 같은 맥락으로 static 메서드 이기 때문에 Theme.getInstance().getThemeColor(); 실행이 가능
public void display() {
String themeColor = Theme.getInstance().getThemeColor();
System.out.println(
"Label [" + text + "] displayed in " + themeColor + " theme."
);
}
}
클라이언트 코드에서는 버튼, 텍스트필드, 라벨에 대한 객체를 생성하고 display 메서드를 호출한다. 간단하게 싱글톤 클래스에서 테마 색을 관리하고 있기에, Theme.getInstance().setThemeColor("dark"); 라인 이후에 다시 display 메서드를 호출하면 dark가 출력된다. 이와 같이 싱글톤 패턴으로 시스템 전역에 적용되어야 하는 설정값을 하나의 객체로 관리 할 수 있다.
// Client code
public class Main {
public static void main(String[] args) {
Button button = new Button("Submit");
TextField textField = new TextField("Enter your name");
Label label = new Label("Username");
// display 메서드는 내부적으로 테마 색을 출력하도록 되어있음
// 기본적으로 light가 기본값이기에, 이시점에서는 light로 출력됨
button.display();
textField.display();
label.display();
// 테마 색을 dark로 변경
Theme.getInstance().setThemeColor("dark");
// 아래 출력문은 테마색이 dark로 변경되어 출력됨
button.display();
textField.display();
label.display();
}
}
singleton 패턴 요약
싱글톤 패턴(Singleton Pattern)은 생성 패턴(Creational Pattern) 중 하나로, 특정 클래스의 인스턴스가 오직 하나만 존재하도록 보장하고, 이 인스턴스에 전역적으로 접근할 수 있게 해주는 디자인 패턴이다. 하지만 위 패턴을 실무에 적용할때는 조금 조심해서 적용해야하는게 멀티스레드 환경에서는 예상치 못한 동기화 문제를 발생시키는 경우도 있으니 조심해서 적용하도록 하자.
-
[java] 메소드
intro : java 메소드에 대한 개념을 알아보자.
메서드 시작
두 숫자를 입력 받아서 더하고 출력하는 단순한 기능을 개발해보자. 먼저 1 + 2 를 수행하고, 그 다음으로 10 + 20 을 수행할 것인데, 아래 코드를 보면 두개의 변수를 받아서 합을 구하는 연산 과정이 중복이 된다. 이 중복되는 부분 어떻게 개선을 하면 좋을까 ?
public class Method1 {
public static void main(String[] args) {
// 계산1
int a = 1;
int b = 2;
System.out.println(a + "+" + b + " 연산 수행");
int sum1 = a + b;
System.out.println("결과1 출력:" + sum1);
// 계산2
int x = 10;
int y = 20;
System.out.println(x + "+" + y + " 연산 수행");
int sum2 = x + y;
System.out.println("결과2 출력:" + sum2);
}
}
메서드 사용
자바에서는 함수를 메서드(Method)라 한다. 메서드도 함수의 한 종류라고 생각하면 된다. 지금은 둘을 구분하지 않고, 이정도만 알아두자. 앞서 작성한 코드의 중복된 부분을 메서드를 이용하여 개선해보자.
public class Method1Ref {
public static void main(String[] args) {
int sum1 = add(5, 10);
System.out.println("결과1 출력:" + sum1);
int sum2 = add(15, 20);
System.out.println("결과2 출력:" + sum2);
}
// add 메서드
public static int add(int a, int b) {
System.out.println(a + "+" + b + " 연산 수행");
int sum = a + b;
return sum;
}
}
아래와 같은 코드에 add 함수를 메서드라고 한다. 메서드의 역할은 어떤 연산을 처리한 다음에 결과를 반환한다. (무조건적인 반환을 해주어야 하는건 아니다.)
public static int add(int a, int b) {
System.out.println(a + "+" + b + " 연산 수행");
int sum = a + b;
return sum;
}
메서드의 구조
메서드의 구조는 크게 2가지로 나눌 수 있는데, 메서드 선언부와 본문으로 나눌 수 있다.
메서드 선언
메서드 이름 반환타입 매개변수 목록을 포함한다. 이름 그대로 이런 메서드가 있다고 선언하는 것이다. 메서드 선언 정보를 통해 다른 곳에서 해당 메서드를 호출할 수 있다.
// add 메서드 선언부
public static int add(int a, int b)
메서드 본문
메서드가 수행해야 하는 코드 블록이다. 메서드를 호출하면 메서드 본문이 순서대로 실행된다. 메서드 본문은 블랙박스이다. 메서드를 호출하는 곳에서는 메서드 선언은 알지만 메서드 본문은 모른다. 메서드의 실행 결과를 반환하려면 return 문을 사용해야 한다. return 문 다음에 반환할 결과를 적어주면 된다.
{
// add 메서드 본문
System.out.println(a + "+" + b + " 연산 수행");
int sum = a + b;
return sum;
}
메서드 호출과 용어정리
메서드를 호출할 때는 다음과 같이 메서드에 넘기는 값과 매개변수(파라미터)의 타입이 맞아야 한다. 물론 넘기는 값과 매개변수(파라미터)의 순서와 갯수도 맞아야 한다.
인수 (Argument)
여기서 "hello" , 20 처럼 메서드에 넘기는 값을 영어로 Argument(아규먼트), 한글로 인수 또는 인자라 한다.실무에서는 아규먼트, 인수, 인자라는 용어를 모두 사용한다.
매개변수(Parameter)
메서드를 정의할 때 선언한 변수인 String str , int age 를 매개변수, 파라미터라 한다. 메서드를 호출할 때 인수를 넘기면, 그 인수가 매개변수에 대입된다. 실무에서는 매개변수, 파라미터 용어를 모두 사용한다.
위 둘의 단어는 반드시 분리되어 사용되어야 한다 혼용되어선 안된다.
메서드 정의
메서드는 다음과 같이 정의 할 수 있다.
public static int add(int a, int b) {
// 메서드 본문, 실행 코드
}
제어자 반환타입 메서드이름(매개변수 목록) {
메서드 본문
}
제어자(Modifier)
public , static 과 같은 부분이다. 제어자는 뒤에서 설명한다. 지금은 항상 public static 키워드를 입력하자.
반환 타입(Return Type)
메서드가 실행 된 후 반환하는 데이터의 타입을 지정한다. 메서드가 값을 반환하지 않는 경우, 없다는 뜻의 void 를 사용해야 한다. 예) void print(String str)
메서드 이름(Method Name)
메서드의 이름이다. 이 이름은 메서드를 호출하는 데 사용된다.
매개변수(Parameter)
입력 값으로, 메서드 내부에서 사용할 수 있는 변수이다. 매개변수는 옵션이다. 입력값이 필요 없는 메서드는 매개변수를 지정하지 않아도 된다. 예) add()
메서드 본문(Method Body)
실제 메서드의 코드가 위치한다. 중괄호 {} 사이에 코드를 작성한다.
반환 타입
반환 타입이 있으면 반드시 값을 반환해야 한다. 반환 타입이 있는 메서드는 반드시 return 을 사용해서 값을 반환해야 한다.
아래 코드에서 if 조건이 만족할 때는 true 가 반환되지만, 조건을 만족하지 않으면 어떻게 될까? 조건을 만족하지 않은 경우에는 return 문이 실행되지 않는다. 따라서 이 코드를 실행하면 return 문을 누락했다는 다음과 같은 컴파일 오류가 발생한다.
public class MethodReturn1 {
public static void main(String[] args) {
boolean result = odd(2);
System.out.println(result);
}
public static boolean odd(int i) {
if (i % 2 == 1) {
return true;
}
// 반드시 boolean 타입으로 return이 되어야 하는 메서드 인데 if문 조건을 만족하지 못한다면
// return 되지 않기에 컴파일 오류가 발생한다.
}
}
위 코드는 다음과 같이 수정되어야 한다.
public class MethodReturn1 {
public static void main(String[] args) {
boolean result = odd(2);
System.out.println(result);
}
public static boolean odd(int i) {
if (i % 2 == 1) {
return true;
} else {
// 조건문을 만족하지 않는다면 false 반환
return false;
}
}
}
return 문을 만나면 그 즉시 메서드를 빠져나간다.
return 문을 만나면 그 즉시 해당 메서드를 빠져나가는데 다음과 같이 활용할 수 있다. (메서드를 특정 시점에 강제로 종료해야할때 자주 사용한다.)
public class MethodReturn2 {
public static void main(String[] args) {
checkAge(10);
checkAge(20);
}
public static void checkAge(int age) {
if (age < 18) {
System.out.println(age + "살, 미성년자는 출입이 불가능합니다.");
// return 문을 만나면 메서드가 종료되고, 아래 출력문이 실행되지 않는다.
return;
}
System.out.println(age + "살, 입장하세요.");
}
}
메서드 호출과 값 전달1
자바에서 중요한 원칙이 있는데, 자바는 값을 대입할때 변수의 값을 복사해서 대입한다. 아래 코드를 통해서 내가 이해한 중요한 원칙을 제대로 이해했는지 확인해보자. 아래 주석으로 출력 결과를 작성했다. 혹시 틀리게 생각한 부분이 있다면 변수의 값을 복사해서 대입한다는 개념이 아직 부족하다는 뜻이다.
public class MethodValue1 {
public static void main(String[] args) {
int num1 = 5;
System.out.println("1. changeNumber 호출 전, num1: " + num1);
changeNumber(num1);
System.out.println("4. changeNumber 호출 후, num1: " + num1);
}
public static void changeNumber(int num2) {
System.out.println("2. changeNumber 변경 전, num2: " + num2);
num2 = num2 * 2;
System.out.println("3. changeNumber 변경 후, num2: " + num2);
}
}
// 1. changeNumber 호출 전, num1: 5
// 2. changeNumber 변경 전, num2: 5
// 3. changeNumber 변경 후, num2: 10
// 4. changeNumber 호출 후, num1: 5
메서드 호출과 값 전달2
다시한번 중요한 원칙인 자바는 값을 복사해서 대입한다라는 개념을 이해했는지 재확인해보자.
public class MethodValue2 {
public static void main(String[] args) {
int number = 5;
System.out.println("1. changeNumber 호출 전, number: " + number); // 출력: 5
changeNumber(number);
System.out.println("4. changeNumber 호출 후, number: " + number); // 출력: 5
}
public static void changeNumber(int number) {
System.out.println("2. changeNumber 변경 전, number: " + number); // 출력: 5
number = number * 2;
System.out.println("3. changeNumber 변경 후, number: " + number); // 출력: 10
}
}
// 1. changeNumber 호출 전, number: 5
// 2. changeNumber 변경 전, number: 5
// 3. changeNumber 변경 후, number: 10
// 4. changeNumber 호출 후, number: 5
메서드 호출과 값 반환받기
메서드를 사용해서 값을 변경하려면 어떻게 해야할까? 메서드의 호출 결과를 반환 받아서 사용하면 된다.
public class MethodValue3 {
public static void main(String[] args) {
int num1 = 5;
System.out.println("changeNumber 호출 전, num1: " + num1); // 출력: 5
num1 = changeNumber(num1);
System.out.println("changeNumber 호출 후, num1: " + num1); // 출력: 10
}
public static int changeNumber(int num2) {
num2 = num2 * 2;
return num2;
}
}
메서드와 형변환
메서드를 호출할 때도 형변환이 적용된다. 메서드 호출과 명시적 형변환, 자동 형변환에 대해 알아보자.
명시적 형변환
메서드를 호출하는데 인자와 매개변수의 타입이 맞지 않다면 어떻게 해야할까? 아래 주석을 풀고 실행하면 다음과 같은 오류가 발생한다. (java: incompatible types: possible lossy conversion from double to int)
public class MethodCasting1 {
public static void main(String[] args) {
double number = 1.5;
// double을 int형에 대입하므로 컴파일 오류
// 반드시 실행하고 싶다면 (int) number 을 인수로 전달해야 한다.
//printNumber(number);
printNumber((int) number); // 명시적 형변환을 사용해 double을 int로 변환
}
public static void printNumber(int n) {
System.out.println("숫자: " + n);
}
}
자동 형변환
int < long < double 메서드를 호출할 때 매개변수에 값을 전달하는 것도 결국 변수에 값을 대입하는 것이다. 따라서 앞서 배운 자동 형변환이 그대로 적용된다. 메서드를 호출할 때는 전달하는 인수의 타입과 매개변수의 타입이 맞아야 한다. 단 타입이 달라도 자동 형변환이 가능한 경우에는 호출할 수 있다.
byte → short → int → long → float → double
char → int → long → float → double
public class MethodCasting2 {
public static void main(String[] args) {
int number = 100;
printNumber(number); // int에서 double로 자동 형변환
}
public static void printNumber(double n) {
System.out.println("숫자: " + n);
}
}
메서드 오버로딩
자바는 메서드의 이름 뿐만 아니라 매개변수 정보를 함께 사용해서 메서드를 구분한다. 따라서 다음과 같이 이름이 같고, 매개변수가 다른 메서드를 정의할 수 있다. 이렇게 이름이 같고 매개변수가 다른 메서드를 여러개 정의하는 것을 메서드 오버로딩(Overloading)이라 한다. 오버로딩은 번역하면 과적인데, 과하게 물건을 담았다는 뜻이다. 따라서 같은 이름의 메서드를 여러개 정의했다고 이해하면 된다.
오버로딩 규칙
메서드의 이름이 같아도 매개변수의 타입 및 순서가 다르면 오버로딩을 할 수 있다. 참고로 반환 타입은 인정하지 않는다.
메서드 시그니처(method signature)
메서드 시그니처 = 메서드 이름 + 매개변수 타입(순서)
메서드 시그니처는 자바에서 메서드를 구분할 수 있는 고유한 식별자나 서명을 뜻한다. 메서드 시그니처는 메서드의 이름과 매개변수 타입(순서 포함)으로 구성되어 있다. 쉽게 이야기해서 메서드를 구분할 수 있는 기준이다. 자바 입장에서는 각각의 메서드를 고유하게 구분할 수 있어야한다. 그래야 어떤 메서드를 호출 할 지 결정할 수 있다. 따라서 메서드 오버로딩에서 설명한 것 처럼 메서드 이름이 같아도 메서드 시그니처가 다르면 다른 메서드로 간주한다. 반환 타입은 시그니처에 포함되지 않는다.
다음 코드는 오버로딩을 활용한 코드이다.
public class Overloading1 {
public static void main(String[] args) {
System.out.println("1: " + add(1, 2));
System.out.println("2: " + add(1, 2, 3));
}
// 첫 번째 add 메서드: 두 정수를 받아서 합을 반환한다.
public static int add(int a, int b) {
System.out.println("1번 호출");
return a + b;
}
// 두 번째 add 메서드: 세 정수를 받아서 합을 반환한다.
// 첫 번째 메서드와 이름은 같지만, 매개변수 목록이 다르다.
public static int add(int a, int b, int c) {
System.out.println("2번 호출");
return a + b + c;
}
}
아래코도는 오버로딩 케이스중에 헷갈릴만한 케이스인데, add 메서드는 파라미터가 실수형 타입 double을 인자값으로 받고있다. 이때 add(1, 2)를 호출하게 된다면 어떻게 될까? 단순히 실수형 타입에 정수형 타입을 넣어야 하니 오류가발생할것 같지만 실제로는 자동형변환이 일어나서 add(1, 2) 가 정상적으로 실행된다.
public class Main {
public static void main(String[] args) {
System.out.println("1: " + add(1, 2));
System.out.println("2: " + add(1.2, 1.5));
}
public static double add(double a, double b) {
System.out.println("add 메서드 호출");
return a + b;
}
}
// add 메서드 호출
// 1: 3.0
// add 메서드 호출
// 2: 2.7
-
[java] 배열
intro : java 배열에 대한 개념을 알아보자.
배열 시작
배열이 필요한 이유
만약 학생의 점수를 출력해야하는 간단한 프로그램을 구성한다고 생각해보자. 각 학생이 추가될때마다 새로운 변수에 점수를 담고, 해당 점수를 출력을 하게되는데, 아직 배열에 대한 개념을 배우기 전이기 때문에 다음과 같은 코드를 통해 조금 힘들게 구성을 해야한다. 이렇게 같은 타입의 변수를 반복해서 선언하고 반복해서 사용하는 문제를 해결할수 있는게 배열이라는 개념이다.
public class Array1 {
public static void main(String[] args) {
int student1 = 90;
int student2 = 80;
int student3 = 70;
int student4 = 60;
int student5 = 50;
System.out.println("학생1 점수: " + student1);
System.out.println("학생2 점수: " + student2);
System.out.println("학생3 점수: " + student3);
System.out.println("학생4 점수: " + student4);
System.out.println("학생5 점수: " + student5);
}
}
배열의 선언과 생성
배열은 같은 타입의 변수를 사용하기 편리하게 하나로 묶어둔 것이다. 위 예제에서는 학생이라는 변수는 같은 int형 타입으로 묶을수 있는데 다음과 같이 코드를 구성할 수 있다.
public class Array1Ref1 {
public static void main(String[] args) {
int[] students; // 배열 변수 선언
students = new int[5]; // 배열 생성
// 변수 값 대입
students[0] = 90;
students[1] = 80;
students[2] = 70;
students[3] = 60;
students[4] = 50;
// 변수 값 사용
System.out.println("학생1 점수: " + students[0]);
System.out.println("학생2 점수: " + students[1]);
System.out.println("학생3 점수: " + students[2]);
System.out.println("학생4 점수: " + students[3]);
System.out.println("학생5 점수: " + students[4]);
}
}
배열의 생성 단계
배열의 생성 단계에는 다음과 같은 단계를 따르는데 배열의 선언, 배열 생성, 배열 참조값 보관을 따른다. 배열의 선언은 int[] students; 이처럼 내가 공통으로 묶고자 하는 타입을 [ ] 대괄호를 이용하여 묶고 변수를 선언하는 것을 말한다. 이후 해당 변수의 실제 생성 부분은 new 연산자를 이용해 new int[5] 배열의 사이즈를 지정한다. 이렇게 배열을 선언하고 생성하게되면 선언한 변수에 생성한 배열 참조값을 보관하게 된다. 참조값이란? 기존 변수 Chapter에서 배운 내용은 기본형 변수 타입 인데, 기본형 변수 타입을 제외한 모든 변수 타입은 참조형 변수 타입으로 주소값(참조값)을 통해 접근할 수 있다.
배열 사용
인덱스
배열은 변수와 사용법이 비슷한데, 차이점이 있다면 다음과 같이 [] 사이에 숫자번호를 넣어주면 된다. 배열의 위치를 나타내는 것을 index(인덱스) 라고 한다. 참고로 배열에서 인덱스는 1이 아니라 0 부터시작한다. 만약 접근 가능한 배열의 인덱스 범위를 넘어가면 java.lang.ArrayIndexOutOfBoundsException 오류가 발생한다.
// 변수 값 대입
students[0] = 90;
students[1] = 80;
// 변수 값 사용
System.out.println("학생1 점수: " + students[0]);
System.out.println("학생2 점수: " + students[1]);
배열에 값 대입
배열에 값을 대입하던지 배열의 값을 사용하던지 일반적인 변수와 사용법은 같다. 추가로 [] 를 통해 접근하고자 하는 배열의 인덱스 값만 넣어주면 된다.
students[0] = 90; // 1. 배열에 값을 대입
x001[0] = 90; // 2. 변수에 있는 참조값을 통해 실제 배열에 접근. 인덱스를 사용해서 해당 위치의 요소에 접근, 값 대입
students[1] = 80; // 1. 배열에 값을 대입
x001[1] = 80; // 2. 변수에 있는 참조값을 통해 실제 배열에 접근. 인덱스를 사용해서 해당 위치의 요소에 접근, 값 대입
배열 값 읽기
배열을 사용할때는 다음과 같이 참조값을 통해서 실제 배열에 접근하고 인덱스를 통해서 원하는 요소를 찾을 수 있다.
// 1. 변수 값 읽기
System.out.println("학생1 점수: " + students[0]);
// 2. 변수에 있는 참조값을 통해 실제 배열에 접근. 인덱스를 사용해서 해당 위치의 요소에 접근
System.out.println("학생1 점수: " + x001[0]); // students 변수의 주소값이 x001 가정
// 3. 배열의 값을 읽어옴
System.out.println("학생1 점수: " + 90);
기본형 vs 참조형
자바의 변수 데이터 타입을 가장 크게 보면 기본형과 참조형으로 분류할 수 있다. 사용하는 값을 직접 넣을 수 있는 기본
형, 그리고 방금 본 배열 변수와 같이 메모리의 참조값을 넣을 수 있는 참조형으로 분류할 수 있다. 기본형(Primitive Type): 우리가 지금까지 봤던 int , long , double , boolean 처럼 변수에 사용할 값을 직접 넣을 수 있는 데이터 타입을 기본형(Primitive Type)이라 한다. 참조형(Reference Type): int[] students 와 같이 데이터에 접근하기 위한 참조(주소)를 저장하는 데이터 타입을 참조형(Reference Type)이라 한다. (위에서 살짝 언급한 내용을 다시 한번 정리한다.)
배열은 왜 참조형을 사용할까 ?
int, long, double 등 기본형 타입 변수들은 선언과 동시에 사이즈가 정해진다. int (4 Byte), long(8 Byte), double(8 Byte) ….. 배열은 생성시점에 동적으로 크기를 지정하게 되는데 예를들면 int[] array = new int[10] 인 경우는 배열의 사이즈가 int 타입이 10개 있기에 40 Byte가 할당된다. 이렇게 참조형 타입을 사용하면 동적으로 크기를 설정할 수 있는 유연성을 제공 할 수 있고 복잡한 데이터 구조도 만들고 관리할 수 있는 장점이 생기기에 배열은 참조형을 사용한다.
배열 리펙토링
아래 코드를 보면 배열의 선언과 생성 부분에서 나왔던 코드랑은 굉장히 비교가 될 정도로 짧아지고 효율적인 코드를 작성할 수 잇게된다. 학생이 추가 될때마다 변수를 새로 생성하고, 출력문을 다시 작성했던것에 비해 아래 코드는 만약 학생이 추가될 경우 students 변수에 값을 하나 더 추가해주면 반복문을 통해 자동으로 새로 추가된 학생의 값 또한 출력 할 수 있게 된다.
public class Array1Ref4 {
public static void main(String[] args) {
//배열 생성 간략 버전, 배열 선언과 함께 사용시 new int[] 생략 가능
int[] students = {90, 80, 70, 60, 50};
for (int i = 0; i < students.length; i++) {
System.out.println("학생" + (i + 1) + " 점수: " + students[i]);
}
}
}
2차원 배열 - 시작
지금까지 학습한 배열은 단순히 순서대로 나열되어 있었다. 이것을 1차원 배열이라 한다. 이번에 학습할 2차원 배열은 이름 그대로 하나의 차원이 추가된다. 2차원 배열은 행과 열로 구성된다. 2차원 배열의 사용법은 [] 가 하나 추가되는 것을 제외하고는 앞서본 1차원 배열과 같다. 아래 코드를 통해 2차원 배열에 대해서 알아보자.
public class ArrayDi0 {
public static void main(String[] args) {
// 2x3 2차원 배열을 만든다.
int[][] arr = new int[2][3]; // 행(row), 열(column)
arr[0][0] = 1; // 0행, 0열
arr[0][1] = 2; // 0행, 1열
arr[0][2] = 3; // 0행, 2열
arr[1][0] = 4; // 1행, 0열
arr[1][1] = 5; // 1행, 1열
arr[1][2] = 6; // 1행, 2열
// 0행 출력
System.out.print(arr[0][0] + " "); // 0열 출력
System.out.print(arr[0][1] + " "); // 1열 출력
System.out.print(arr[0][2] + " "); // 2열 출력
System.out.println(); // 한 행이 끝나면 라인을 변경한다.
// 1행 출력
System.out.print(arr[1][0] + " "); // 0열 출력
System.out.print(arr[1][1] + " "); // 1열 출력
System.out.print(arr[1][2] + " "); // 2열 출력
System.out.println(); // 한 행이 끝나면 라인을 변경한다.
}
}
2차원 배열 - 리펙토링1
위 2차원 배열에서의 코드를 보면 중복된 부분이 존재한다. 0행과 1행을 출력하는 코드의 내용을 자세히보면 결국 2차원 배열에서의 행과 열에 대한 인덱스를 순차적으로 접근하여 출력하면 된다는 것을 눈치 챌 수 있다. 아래 코드처럼 2중 반복문을 통해 순차적으로 2차원 배열의 인덱스를 활용하여 코드를 깔끔하게 작성할 수 있다.
public class ArrayDi2 {
public static void main(String[] args) {
// 2x3 2차원 배열을 만듭니다.
int[][] arr = new int[2][3]; // 행(row), 열(column)
arr[0][0] = 1; // 0행, 0열
arr[0][1] = 2; // 0행, 1열
arr[0][2] = 3; // 0행, 2열
arr[1][0] = 4; // 1행, 0열
arr[1][1] = 5; // 1행, 1열
arr[1][2] = 6; // 1행, 2열
for (int row = 0; row < 2; row++) {
for (int column = 0; column < 3; column++) {
System.out.print(arr[row][column] + " ");
}
System.out.println(); // 한 행이 끝나면 라인을 변경한다.
}
}
}
2차원 배열 - 리펙토링2
리팩토링 1번에서의 부족한 부분을 조금더 찾아보면 배열의 선언과 초기화하는 부분에서 조금 아쉬운 부분이 존재한다, 2중 반복문에서 조건식에 해당하는 부분의 코드가 하드코딩으로 행과 열의 값 2, 3 으로 작성되어 있는게 아쉽다. 배열은 동적으로 크기가 변할 수 있기 때문에 변경 포인트를 최대한 줄이는게 유지보수 측면에서도 좋기에 위 문제를 리팩토링하여 개선한 코드는 다음과 같이 작성할 수 있다.
public class ArrayDi3 {
public static void main(String[] args) {
// 2x3 2차원 배열, 초기화
// 배열의 생성시 new int[2][3] 대신에 다음과 같이 {} 를 이용해 직접 작성할 수 있다.
int[][] arr = {
{1, 2, 3},
{4, 5, 6}
};
// 2차원 배열의 길이를 활용
// 행과 열의 크기를 배열의 length 메소드를 통해 지정
for (int row = 0; row < arr.length; row++) {
for (int column = 0; column < arr[row].length; column++) {
System.out.print(arr[row][column] + " ");
}
System.out.println();
}
}
}
향상된 for문 (배열같은 순차적인 데이터에 접근해야할때 유용하다)
보통 for-each 문으로 부르며 향상된 for문은 기존의 반복문 보다 더 편리하게 사용할 수 있다. 물론 모든 상황에서 편리하게 사용할 수 있는건 아니지만 대부분의 상황에서는 향상된 for문을 사용한다. (index 값을 사용해야 하는 경우는 향상된 for문이 아니라, 기존에 사용하던 for문을 사용하자)
향상된 for문 정의
for (변수 : 배열 또는 컬렉션) {
// 배열 또는 컬렉션의 요소를 순회하면서 수행할 작업
}
향상된 for문 예제
public class EnhancedFor1 {
public static void main(String[] args) {
int[] numbers = {1, 2, 3, 4, 5};
//일반 for문
for(int i = 0; i < numbers.length; ++i) {
int number = numbers[i];
System.out.println(number);
}
//향상된 for문, for-each문
for (int number : numbers) {
System.out.println(number);
}
//for-each문을 사용할 수 없는 경우, 증가하는 index 값 필요
for(int i = 0; i < numbers.length; ++i) {
System.out.println("number" + i + "번의 결과는: " + numbers[i]);
}
}
}
-
[design-pattern] template method 패턴
intro : template method 패턴에 대해서 알아보자
Template Method 패턴이란 ?
Template Method 패턴은 객체지향 설계에서 자주 사용하는 행동(Behavioral) 디자인 패턴 중 하나로, 상위 클래스에서 알고리즘의 구조를 정의하고, 하위 클래스에서 일부 세부적인 동작을 구체적으로 구현하도록 허용하는 패턴이다. 즉, 알고리즘의 골격을 정의하고 세부적인 내용은 서브클래스에서 완성되도록 유도한다.
Template Method 패턴은 언제 사용하는가 ?
Template Method 패턴은 알고리즘의 전체 구조(골격)는 고정하되, 특정 단계만 커스터마이징해야 할 때 사용됩니다. 이 패턴은 공통된 프로세스를 상위 클래스에 정의하고, 세부적인 동작을 하위 클래스에서 구현하도록 한다.
Template Method 패턴 적용 코드 (1)
골격을 정의할 Beverage 추상 클래스, 차후 Beverage를 구현할 하위 클래스가 세부 역할을 정의하게 된다.
abstract class Beverage {
// Template method (큰 틀과 골격, 이부분은 변하지 않는다.)
// 하위 클래스에서 재정의가 되지 않도록 final 키워드를 사용하였다.
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
void boilWater() {
System.out.println("Boiling water");
}
void pourInCup() {
System.out.println("Pouring into cup");
}
// 하위 클래스에서 정의할 메소드들
abstract void brew();
abstract void addCondiments();
}
Beverage 추상 클래스를 상속받아 구현한 Tea, Coffee 클래스이다. 해당 클래스들은 brew, addCondiments 메서드들을 각각 구현한다.
// 실제 구현체 클래스에서 추상 메서드 재정의
class Tea extends Beverage {
void brew() {
System.out.println("Steeping the tea");
}
void addCondiments() {
System.out.println("Adding lemon");
}
}
// 실제 구현체 클래스에서 추상 메서드 재정의
class Coffee extends Beverage {
void brew() {
System.out.println("Dripping coffee through filter");
}
void addCondiments() {
System.out.println("Adding sugar and milk");
}
}
클라이언트는 다음과 같이 Beverage 타입으로 Tea, Coffee 객체를 생성한다. (다형성의 특징) Beverage 클래스에 미리 정의된 prepareRecipe 메서드는 내부적으로 고유한 골격과 순서를 가지고 잇기 때문에 어떠한 구현체 클래스가 prepareRecipe 메서드를 호출하더라도 메서드 내부의 순서는 다르지 않고, brew, addCondiments 메서드의 Overriding 내용만 다르게 작동한다.
// Client code
public class Main {
public static void main(String[] args) {
Beverage tea = new Tea();
Beverage coffee = new Coffee();
System.out.println("\nMaking tea...");
tea.prepareRecipe();
System.out.println("\nMaking coffee...");
coffee.prepareRecipe();
}
}
Template Method 패턴 적용 코드 (2)
위 내용과 비슷한 상황이지만, Template Method가 분기에 따라 다르게 동작하는 상황에 대해서도 적용할 수 있다. (보다 실무에 가까운 예제)
// 해당 패턴이 인터페이스를 사용하지 않는 이유는
// 템플릿 메서드는 상위 클래스에서 구현이 되어 있어야 하기 때문.
// 추상클래스의 사용 이유가 된다.
abstract class DataProcessor {
// Template method (큰 틀과 골격, 이부분은 변하지 않는다.)
// 하위 클래스에서 재정의가 되지 않도록 final 키워드를 사용하였다.
public final void process(String data) {
loadData(data);
if (isValidData(data)) {
processData(data);
saveData(data);
} else {
System.out.println("Data is invalid, processing aborted.");
}
}
// 추상 메서드를 정의하고 하위 클래스에서 구현할수 있도록 정의
// protected 키워드는 자식 클래스에서는 접근가능한 접근 제어자
protected abstract void loadData(String data);
protected abstract boolean isValidData(String data);
protected abstract void processData(String data);
protected abstract void saveData(String data);
}
BeveraDataProcessorge 추상 클래스를 상속받아 구현한 CSVDataProcessor, JSONDataProcessor 클래스이다. 해당 클래스들은 loadData, isValidData, processData, saveData 메서드들을 각각 구현한다. 위 예제와 같은 역할이다 결국 상위 클래스에서 정의한 추상 메서드들을 각 클래스의 역할에 맞게 재정의 할 뿐이다.
// 실제 구현체 클래스에서 추상 메서드 재정의
class CSVDataProcessor extends DataProcessor {
@Override
protected void loadData(String data) {
System.out.println("Loading data from CSV file: " + data);
}
@Override
protected boolean isValidData(String data) {
return data != null && data.contains("CSV");
}
@Override
protected void processData(String data) {
System.out.println("Processing CSV data");
}
@Override
protected void saveData(String data) {
System.out.println("Saving CSV data to database");
}
}
// 실제 구현체 클래스에서 추상 메서드 재정의
class JSONDataProcessor extends DataProcessor {
@Override
protected void loadData(String data) {
System.out.println("Loading data from JSON file: " + data);
}
@Override
protected boolean isValidData(String data) {
return data != null;
}
@Override
protected void processData(String data) {
System.out.println("Processing JSON data");
}
@Override
protected void saveData(String data) {
System.out.println("Saving JSON data to database");
}
}
클라이언트 코드 즉 Main에서 DataProcessor 타입으로 CSVDataProcessor, JSONDataProcessor 객체들을 사용한다. 이때 실행하는 메서드 process 는 추상클래스인 DataProcessor의 템플릿 메서드 이므로, 상위클래스에서 정의된 순서대로 실행된다. (템플릿 메서드 안에 내부적으로 메서드가 순차적으로 정의 되어 있음)
// Client code
public class Main {
public static void main(String[] args) {
DataProcessor csvProcessor = new CSVDataProcessor();
csvProcessor.process("CSV data");
System.out.println();
DataProcessor jsonProcessor = new JSONDataProcessor();
jsonProcessor.process("XML data");
}
}
Template Method 패턴 요약
Template Method 패턴은 알고리즘의 골격(공통된 흐름)을 상위 클래스에서 정의하고, 세부적인 구현은 하위 클래스에서 담당하게 하는 행동(Behavioral) 디자인 패턴이다.
-
[java] 스코프, 형변환
intro : java 스코프, 형변환에 대한 개념을 알아보자.
스코프1 - 지역 변수와 스코프
변수는 선언한 위치에 따라 지역 변수, 멤버 변수(클래스 변수, 인스턴스 변수)와 같이 분류된다. 지역 변수는 이름 그대로 특정 지역에서만 사용할 수 있는 변수라는 뜻이다. 그 특정 지역을 벗어나면 사용할 수 없다. 여기서 말하는 지역이 바로 변수가 선언된 코드 블록 ({ })이다. 지역 변수는 자신이 선언된 코드 블록 안에서만 생존하고, 자신이 선언된 코드 블록을 벗어나면 제거된다. 따라서 이후에는 접근 할 수 없다.
public class Scope1 {
public static void main(String[] args) {
int m = 10; //m 생존 시작
if (true) {
int x = 20; //x 생존 시작
System.out.println("if m = " + m); //블록 내부에서 블록 외부는 접근 가능
System.out.println("if x = " + x);
} //x 생존 종료
//System.out.println("main x = " + x); //오류, 변수 x에 접근 불가
System.out.println("main m = " + m);
} //m 생존 종료
}
정리하자면 지역변수는 본인의 코드블록 안에서만 생존하고 자신의 코드블록을 벗어나면 제거되기 때문에 접근할 수 없다. 이렇게 변수의 접근 가능한 범위를 스코프(Scope) 라고 한다. 참고로 번역하면 범위라는 뜻이다. 아래 코드에서는 변수 m은 main 메소드 안에서 접근 가능한 지역변수 이지만, 변수 i는 for문 안에서만 접근 가능한 지역변수이다.
public class Scope2 {
public static void main(String[] args) {
int m = 10;
for (int i = 0; i < 2; i++) { //블록 내부, for문 내
System.out.println("for m = " + m); //블록 내부에서 외부는 접근 가능
System.out.println("for i = " + i);
} //i 생존 종료
//System.out.println("main i = " + i); //오류, i에 접근 불가
System.out.println("main m = " + m);
}
}
스코프2 - 스코프 존재 이유
변수를 선언한 시점부터 변수를 계속 사용할 수 있께 해도 되지 않을까? 왜 복잡하게 접근 범위라는 개념을 만들었을까?
public class Scope3_1 {
public static void main(String[] args) {
int m = 10;
int temp = 0;
if (m > 0) {
temp = m * 2;
System.out.println("temp = " + temp);
}
System.out.println("m = " + m);
}
}
위 코드를 보면 변수 m을 2배로 증가해서 출력하는 코드이다. 여기서 두배 증가한 값을 저장하기 위해 변수 temp를 사용하였는데, 해당 변수의 사용범위는 if 조건 안에서만 사용된다. 그런데 현재 temp 변수는 main 코드 블록안에 선언되어 있다. 이런경우 다음과 같은 문제가 발생한다.
비효율적 메모리 사용
temp는 if 코드 블록에서만 필요하지만 main 코드 블록이 종료될떄까지 메모리에 유지된다. 따라서 불필요한 메모리가 낭비된다. 만약 if 조건안에 temp를 선언했다면 더 효율적으로 메모리를 사용할 수 있다.
코드 복잡성 증가
좋은 코드는 군더더기가 없는 코드이다. temp변수는 if 조건 안에서만 필요하고 여기서만 사용하면 된다. 만약 if 조건안에서 temp 변수를 선언했다면 if 가 끝나고 temp를 전혀 생가하지 않아도 된다. 현재 위 코드는 간단한 코드이기에 복잡성이 증가한다는 말에 동의하기 어렵겠지만 실무환경에서는 하나라도 복잡성을 줄이는게 개발 생산성에 도움을 준다.
while문 vs for문 - 스코프 관점
아래 코드의 변수 스코프 관점에서 카운터 변수 i를 살펴보자. while문 예쩨에서는 i가 main 메소드 안에 선언되어 있고, for문 예제에서는 for문안에 선언되어 있다. 이런경우는 스코프의 제한이 for문이 더 제한적이기에 메모리 효올과 유지보수 관점에서 더 좋은점을 가지고 갈 수 있다.
// while문 예제
public class While2_3 {
public static void main(String[] args) {
int sum = 0;
int i = 1;
int endNum = 3;
while (i <= endNum) {
sum = sum + i;
System.out.println("i=" + i + " sum=" + sum);
i++;
}
//... 아래에 더 많은 코드들이 있다고 가정
}
}
// for문 예제
public class For2 {
public static void main(String[] args) {
int sum = 0;
int endNum = 3;
for (int i = 1; i <= endNum; i++) {
sum = sum + i;
System.out.println("i=" + i + " sum=" + sum);
}
//... 아래에 더 많은 코드들이 있다고 가정
}
}
형변환1 - 자동 형변환
작은 범위에서 큰 범위로는 당연히 값을 넣을 수 있다.(int 👉 long 👉 double) 큰 범위에서 작은 범위는 다음과 같은 문제가 발생할 수 있다. (소수점버림, 오버플로우)
작음 범위에서 큰 범위로 대입은 허용한다.
자바에서 숫자를 표현할수 있는 범위는 다음과 같다. int < long < double int 보다는 long 이, long 보다는 double 이 더 큰 범위를 표현할 수 있다. 작은 범위에서 큰 범위에 값을 대입하는 코드는 실행하면 특별한 문제없이 잘 수행된다. 자바는 기본적으로 같은 타입에 값을 대입할 수 있다. 그런데 다른 타입에 값을 대입하면 어떻게 될까? int long 을 비교해보면 long 이 int 보다 더 큰 숫자 범위를 표현한다. 작은 범위 숫자 타입에서 큰 범위 숫자 타입에 대입을 하면 문제가 되지 않는다. 만약 이런 경우까지 오류가 발생한다면 개발이 너무 불편할 것이다. long double 의 경우에도 double 은 부동 소수점을 사용하기 때문에 더 큰 숫자 범위를 표현한다. 따라서 대입할 수 있다. 정리하면 작은 범위에서 큰 범위로의 대입은 자바 언어에서 허용한다. 쉽게 이야기하면 큰 그릇은 작은 그릇에 담긴 내용물을 담을 수 있다.
public class Casting1 {
public static void main(String[] args) {
int intValue = 10;
long longValue;
double doubleValue;
longValue = intValue; // int -> long
System.out.println("longValue = " + longValue); //longValue = 10
doubleValue = intValue; // int -> double
System.out.println("doubleValue1 = " + doubleValue); //doubleValue1 = 10.0
doubleValue = 20L; // long -> double
System.out.println("doubleValue2 = " + doubleValue); //doubleValue2 = 20.0
}
}
자동형변환 (묵시적 형변환)
하지만 결국 대입하는 형(타입)을 맞추어야 하기 때문에 개념적으로는 다음과 같이 동작한다.
doubleValue = intValue
doubleValue = (double) intValue //형 맞추기
doubleValue = (double) 10 //변수 값 읽기
doubleValue = 10.0 //형변환
형변환2 - 명시적 형변환
큰 범위에서 작은 범위 대입은 명시적 형변환이 필요하다 만약 실수형 데이터를 정수형 타입에 담으면 어떻게 될까? double 타입의 데이터를 int에 담아보겠다. 명시걱으로 타입을 지정해서 형변환을 해주지 않으면 컴파일 오류가 발생한다.
public class Casting2 {
public static void main(String[] args) {
double doubleValue = 1.5;
int intValue = 0;
//intValue = doubleValue; //컴파일 오류 발생
intValue = (int) doubleValue; //형변환
System.out.println(intValue); //출력:1
}
}
형변환
하지만 만약 이런 위험을 개발자가 직접 감수하고도 값을 대입하고 싶다면 데이터 타입을 강제로 변경할 수 있다. 예를들어서 대략적인 결과를 보고싶은데 이때 소수점을 버리고 정수로만 보고 싶을 수 있다. 우리는 아래 코드와 같이 개발자가 직접 타입을 변환하는 것을 명시적 형변환 이라고 한다.
double doubleValue = 1.5;
intValue = (int) doubleValue; //형변환, intValue에 1이 할당 됨
형변환과 오버플로우
형변환을 할때 만약 작은 숫자가 표현할 수 있는 범위를 넘어서면 어떻게 될까? 아래코드에서 변수 maxIntOver 값을 intValue에 int 타입으로 변환하게되면 오버플로우가 발생한다. 정수형에서의 오버플로우는 int 타입으로 담을수 없는 큰 값이 들어오는경우 발생한다. 그렇기에 오버플로우가 발생하지 않도록 변수의 타입을 적절하게 잘 지정해 주어야 한다.
public class Casting3 {
public static void main(String[] args) {
long maxIntValue = 2147483647; //int 최고값
long maxIntOver = 2147483648L; //int 최고값 + 1(초과)
int intValue = 0;
intValue = (int) maxIntValue; //형변환
System.out.println("maxIntValue casting=" + intValue); //출력:2147483647
intValue = (int) maxIntOver; //형변환
System.out.println("maxIntOver casting=" + intValue); //출력:-2147483648
}
}
오버플로우, 언더플로우 조금만 더 알아보자!
정수 (시계열이라고 생각하자)
오버플로우 : 최댓값을 넘으면 최솟값으로 되돌아갑니다.
언더플로우 : 최솟값을 넘으면 최댓값으로 되돌아갑니다.
실수 (표현 못하는 숫자가 되니까 무한대 혹은 0으로 표현한다고 생각하자)
오버플로우 : 값이 너무 커지면 Inf(무한대)가 됩니다.
언더플로우 : 값이 너무 작아지면 서브노멀 값으로 표현되거나 0에 가까워집니다.
계산과 형변환
형변환은 대입 뿐만 아니라, 계산을 할 때도 발생한다. 알고리즘 문제 풀이시 오버플로우가 발생한다면 해당 개념이 부족해서 많이 발생하는거라고 볼 수 있다. (사실 내 얘기다…) 자바에서 계산은 다음과 같은 2가지를 기억하면 된다. 같은 타입끼리의 계산은 같은 타입의 결과를 낸다, 서로다른 타입의 계산은 큰 범위로 자동형변환이 일어난다.
public class Casting4 {
public static void main(String[] args) {
int div1 = 3 / 2;
System.out.println("div1 = " + div1); //1
double div2 = 3 / 2; // 정수끼리의 연산결과를 실수형 double에 담을 뿐이기에 결과는 1.0 이다.
System.out.println("div2 = " + div2); //1.0
double div3 = 3.0 / 2; // 실수형과, 정수형의 연산은 실수형의 결과로 반환된다.
System.out.println("div3 = " + div3); //1.5
double div4 = (double) 3 / 2;
System.out.println("div4 = " + div4); //1.5
int a = 3;
int b = 2;
double result = (double) a / b;
System.out.println("result = " + result); //1.5
}
}
-
[jpa] 연관관계 매핑 기초
intro : jpa의 연관관계 매핑에 대해 알아보자.
연관관계가 필요한 이유
다음과 같은 상황에 대해서 생각해보자. 회원과 팀이 있고 회원은 하나의 팀에만 소속될 수 있다. 회원과 팀은 다대일 관계이다. 이 내용을 토대로 객체를 테이블에 맞추어 모델링한 결과는 다음과 같다.
참조 대신에 외래키를 그대로 사용하여 Entity를 구성
외래키 식별자를 직접 다룸
식별자로 다시 조회, 객체 지향적인 방법은 아님
위 방법이 왜 객체지향적인 방법이 아니라고 말하는걸까 ?
객체는 참조를 사용해서 연관된 객체를 찾는다.
구성된 클래스에서 teamId를 통해 Member 클래스와 Team 클래스의 연관관계를 지정하고 있다. 이는 사실상 데이터베이스의 연관관계 방식을 객체에 그대로 적용한 것으로, 객체지향적 관계와는 거리가 멀다고 할 수 있다. 데이터베이스 중심의 매핑은 객체의 본질적인 특징인 캡슐화와 추상화를 무시하고, 단순히 데이터 저장소의 구조를 따라가는 방식이다. JPA를 활용할 때는 객체지향적 연관관계 매핑을 통해 객체의 본질을 유지하고 데이터 처리의 복잡성을 줄이는 것이 중요하다. 객체의 연관성을 직접 매핑하고, 이를 통해 자연스러운 탐색과 유지보수 가능한 설계를 구현해야 한다. 이는 객체지향적인 코드와 데이터베이스 간의 간극을 효과적으로 줄여주며, 장기적으로 더 나은 성능과 가독성을 제공한다.
단방향 연관관계
위 상황과 다르게 이번엔 객체 중심으로 모델링한 결과는 다음과 같다.
객체의 참조와 테이블의 외래 키를 매핑 (위 상황과 다르게 객체 자체로 키 매핑)
연관관계 저장
참조로 연관관계 조회 - 객체 그래프 탐색
연관관계 수정
양방향 연관관계와 연관관계 주인
이번에는 객체가 단방향 연관관계가 아니라, 양방향 연관관계인 경우이다. 테이블은 외래키 하나로 양쪽 테이블의 데이터를 조회할 수 있지만, 단방향으로 설정된 객체에서는 Member는 Team을 알 수 있어도 Team에서는 Member를 알 수 없다.
Member 엔티티는 단방향 동일
Team 엔티티는 컬렉션 추가 (members), mappedBy 사용
반대 방향으로 객체 그래프 탐색
연관관계 주인과 mappedBy
객체와 테이블간에 연관관계를 맺는 차이를 이해해야 한다.
객체의 연관관계는 2개이다, 회원에서 팀으로의 연관관계 + 팀에서 회원으로의 연관관계 2개. 테이블 관점에서는 회원에서 팀의 pk값을 가지고 있기에 연관관계는 하나지만 양방향으로 조회할 수 있다. 정리하자면 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다 그렇다면 객체에서 또한 어느 곳에서 하나로 외래키를 관리해야 한다. 이때 사용하는것이 mappedBy인데, 주인이 아닌곳에 속성을 사용하게 된다. 보통 주인을 결정할 때는 외래키가 있는 곳을 주인으로 정한다. 위 상황 에서는 Member.team이 연관관계의 주인이 된다.
양방향 매핑시 가장 많이 하는 실수
양방향 매핑시 연관관계의 주인에 값을 입력해야 한다.
순수한 객체 관계를 고려한다면 항상 양쪽 다 값을 입력해야 한다. (Member에 Team을 설정해 줘야 한다.)
양방향 연관관계 주의!
순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하고, 실수할 수도 있으니 연관관계 편의 메소드를 생성해서 사용하자 (권장) set 보다 change 라는 단어가 포함된 이름의 메소드를 구성하는게 관례이며 보통 연관관계 메소드는 한쪽에만 작성한다. 추가적으로 양방향 매핑시에는 무한루프를 조심해야 하는데 toString(), lombok, JSON 생성 라이브러리 등 무한 루프에 빠질 수 있다.
양방향 매핑 정리
단방향 매핑 만으로도 이미 연관관계 매핑은 가능하다. 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다. JPQL 에서 역방향으로 탐색할 일이 많다. 단방향 매핑을 잘 하고 양방향은 필요할때 추가하는게 더 좋다. (어차피 테이블 구성에 영향을 주지 않는다.)
연관관계의 주인을 정하는 기준
비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안되고, 외래키의 위치를 기준으로 정해야한다. 보통 외래키가 있는곳이 연관관계의 주인이다. 객체와 테이블의 모델링을 그림으로 한번 그려보면 외래키가 어디에 있어야 하는지 눈에 쉽게 들어오니 그려보고 판단하는게 좋아 보인다.
-
[jpa] 엔티티 매핑
intro : jpa의 엔티티 매핑에 대해 알아보자.
엔티티 매핑 소개
객체와 테이블 매핑 : @Entity, @Table
필드와 컬럼 매핑 : @Column
기본 키 매핑 : @Id
연관관계 매핑 : @ManyToOne, @JoinColumn
객체와 테이블 매핑
@Entity
@Entity가 붙은 클래스는 JPA가 관리 엔티티라 한다.
JPA를 사용해서 테이블과 매핑할 클래스는 @Entity필수.
주의사항
기본 생성자는 반드시 존재해야한다(public, protect 생성자)
final 클래스, enum, interface, inner 클래스 사용X
저장할 필드에 final 사용
@Entity 속성 정리
속성 : name
JPA에서 사용할 엔티티 이름을 지정한다. 클래스 이름을 그대로 사용.(클래스 이름을 테이블 이름으로 사용함)
@Table
@Table은 엔티티와 매핑할 테이블 지정
속성 : name ➢ 매핑할 테이블 이름
속성 : catalog ➢ 데이터베이스 catalog 매핑
속성 : schema ➢ 데이터베이스 schema 매핑
속성 : uniqueConstraints ➢ DDL 생성시에 유니크 제약 조건 생성
데이터베이스 스키마 자동 생성
DDL을 애플리케이션 실행 시점에 자동 생성. 테이블중심에서 객체중심으로 변환. 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적합한 DDL 생성. 이렇게 생성된 DDL은 개발 장비에서만 사용. 생성된 DDL은 운영서버에서는 사용하지 않거나. 적절히 다듬은 후 사용
hibernate.hbm2ddl.auto의 옵션값으로 하기와 같은 다양한 값을 설정할 수 있다.
옵션 : create ➢ 기존테이블 삭제 후 다시 생성 (DROP + CREATE)
옵션 : create-drop ➢ create와 같으나 종료시점에 테이블 DROP
옵션 : update ➢ 변경분만 반영 (운영DB에는 사용하면 안됨)
옵션 : validate ➢ 엔티티와 테이블이 정상 매핑되었는지만 확인
옵션 : none ➢ 사용하지 않음
데이터베이스 스키마 자동 생성 - 주의
운영 장비에는 절대 create, create-auto, update 사용하면 안된다.
개발 초기 단계는 create 또는 update
테스트 서버는 update 또는 validate
스테이징과 운영 서버는 validate 또는 none
DDL 생성기능
제약 조건 추가 : 회원 이름은 필수 (10자 초과 X)
@Column(nullable = false, length = 10)
유니크 제약조건 추가
@Column(unique = true)
필드와 컬럼 매핑
매핑 어노테이션 정리
@Column : 컬럼 매핑
속성 : name ➢ 필드와 매핑할 테이블의 컬럼 이름
속성 : insertable, updateable ➢ 등록 변경 가능 여부
속성 : nullable ➢ null 값의 허용 여부 설정 false 설정시 not null 제약 조건이 붙음
속성 : unique ➢ 간단히 유니크 제약 조건 걸때 사용
속성 : columnDefinition ➢ 데이터베이스 컬럼 정보를 직접 줄 수 있음 (varchar(100))
속성 : length ➢ 문자 길이 제약조건, String 타입에만 사용
속성 : precision,scale ➢ BigDecimal 같은 타입에서만 사용
@Temporal : 날짜 타입 매핑
날짜 타입 (Date, Calendar)를 매핑할때 사용. LocalDate, LocalDateTIme을 사용할때는 생략 가능.
속성 : value ➢ TemporalType.DATE (날짜 데이터베이스 date 타입과 매핑)
속성 : value ➢ TemporalType.TIME (시간 데이터베이스 time 타입과 매핑)
속성 : value ➢ TemporalType.TIMESTAMP (날짜 데이터베이스 timestamp 타입과 매핑)
@Enumerated : enum 타입 매핑 (ORDINAL 사용 X)
속성 : value ➢ EnumType.STRING (enum 이름을 데이터 베이스에 저장)
속성 : value ➢ EnumType.ORDINNAL (enum 순서를 데이터 베이스에 저장)
@Lob : BLOB(바이너리 데이터), CLOB(문자 데이터) 매핑 - 큰 데이터 타입 지정할때 사용
매핑하는 필드 타입이 문자면 CLOB 매핑, 나머지는 BLOB 매핑
@Transient : 특정 필드를 컬럼에 매핑하지 않음 (매핑 무시)
필드 매핑 X
데이터베이스에 저장 X 조회 X
주로 메모리상에 임시로 어떤 값을 보관하고 싶을 때 사용
기본 키 매핑
기본 키 매핑 어노테이션
@Id, @GeneratedValue
기본 키 매핑 방법
직접 할당: @Id만 사용
자동 생성: @GeneratedValue
IDENTITY 전략 - 특징
IDENTITY : 데이터베이스에 위임 MYSQL
기본 키 생성을 데이터베이스에 위임. 주로 MYSQL, PostgreSQL, SQL Server, DB2에서 사용 AUTO_INCREMENT는 데이터베이스에 INSERT SQL을 실행한 이후에 ID 값을 알 수 있음.
예외적으로 JPA는 IDENTITY 전략을 사용하는 경우 em.persite() 하는 시점에서 미리 SQL 실행 후, 식별자 조회를 한다 그 이유는 SQL 을 실행하고나서 PK 값을 알 수 있기 떄문에 JPA 내부적으로 SQL을 선 실행하고, PK 값을 영속성 컨텍스트에 담는다.
SEQUENCE 전략 - 특징
SEQUENCE : 데이터베이스 시퀀스 오브젝트 사용, ORACLE
데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트
SEQUENCE 전략 - 매핑
@SequenceGenerator
속성 : name ➢ 식별자 생성기 이름
속성 : sequenceName ➢ 데이터베이스에 등록되어 있는 시퀀스 이름
속성 : initialValue ➢ 처음 시작 값 보통 1로 지정
속성 : allocationSize ➢ 시퀀스값이 1씩 증가로 설정되어 있다면 1로 설정해야함 (성능 최적화에 사용)
속성 : catalog, schema ➢ 데이터 베이스 catalog, schema 이름
TABLE 전략 - 매핑
TABLE : 키 생성용 테이블 사용, 모든 DB에서 사용
키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략
장점 : 모든 데이터베이스에 적용 가능
단점 : 성능이 안좋음
AUTO 전략
AUTO : 방언에 따라 자동 지정, 기본값 (따로 설정 안해도 됨)
권장하는 식별자 전략
기본 키 제약 조건 : null 아님, 유일, 변하면 안된다.
미래까지 이 조건을 만족하는 자연키는 찾기 어렵다. 대리키를 사용하자.
예를들어 주민등록번호도 기본키로 적절하지 않다.
권장: Long형 + 대체키 + 키 생성전략 사용
-
[java] 반복문
intro : java 반복문에 대한 개념을 알아보자.
반복문 시작
반복문은 이름 그대로 특정 코드를 반복해서 실행할 떄 사용한다. 자바는 다음 3가지 종류의 반복문을 제공한다.
while , do-while , for
위와 같은 반복문들을 어떤 상황에서 어떻게 사용 할 수 있는지 차근차근 알아보자.
while문 1
while 문은 조건에 따라 코드를 반복해서 실행할 때 사용한다. 조건식을 확인 하였을 때 참이면 코드 블럭을 실행하고 거짓이면 while문을 벗어난다. 만약 조건식이 참 인경우 코드 블록을 실행한 이후에 다시 조건식 검사로 들어가서 조건식을 검사한다.무한반복
while (조건식) {
// 코드
}
좀 더 실제 개발 환경에서 만나볼 수 있을 것 같은 예제를 보면 다음과 같은 코드를 참고 할 수 있다.
public class While1_2 {
public static void main(String[] args) {
int count = 0;
while (count < 3) {
count++;
System.out.println("현재 숫자는:" + count);
}
}
}
while문 2
while 문을 좀더 다양한 상황에 적용해보자, 만약 1부터 3까지 값을 더해야 하는 상황이 있다고 가정해보겠다 while문으로 어떻게 코드를 작성하면 좋을까 ?
public class While2_3 {
public static void main(String[] args) {
int sum = 0;
int startNum = 1;
int endNum = 3;
while (startNum <= endNum) { // startNum이 endNum(3) 이 될때까지 while문 반복
sum = sum + startNum;
System.out.println("startNum=" + startNum + " sum=" + sum);
startNum++;
}
}
}
do-while문
do-while문은 while문과 비슷하지만, 조건에 상관없이 무조건 한 번은 코드를 실행한다.
do {
// 코드 (무조건 한번은 실행된다, 이후에 while문의 조건식이 참인 경우 do 문장을 다시 실행하게 된다.)
} while (조건식);
예를 들어서 조건에 만족하지 않아도 한번은 현재 값을 출력하고 싶다고 하자. 먼저 while 문을 사용한 예제를 보겠다.
public class DoWhile1 {
public static void main(String[] args) {
int i = 10;
while (i < 3) {
System.out.println("현재 숫자는:" + i);
i++;
}
}
}
while 문의 조건에 부합하지 않기때문에 false 가 나오게 되고, 아무것도 출력되지 않는 실행결과를 볼 수 있다. 이번에는 do-while문을 사용해 보겠다. 아래와 같이 코드를 작성한다면, do-while은 do 블록의 코드를 먼저 실행하고 while문의 조건을 검사 후, 참 이면 do 블록의 코드를 실행하고 거짓 이면 do 블록의 코드를 실행하지 않는다. 그렇기에 아래 코드는 현재 숫자는: 10 이라는 결과를 출력하게 된다.
public class DoWhile2 {
public static void main(String[] args) {
int i = 10;
do {
System.out.println("현재 숫자는:" + i);
i++;
} while (i < 3);
}
}
결론적으로 do-while문은 최초 한번은 코드 블럭을 꼭 실행해야 하는 경우에 사용하면 된다.
break, continue
break와 continue는 반복문에서 사용할 수 있는 키워드다. break문은 반복문을 즉시 종료하고 나간다 (가장 가까운 반복문을 탈출한다) continue는 반복문의 나머지 부분을 건너뛰고 다음 반복문으로 진행하는데 사용한다. 해당 키워드 들은 while , do-while , for 와 같은 모든 반복문에서 사용할 수 있다.
break
아래 코드에서 코드1 실행후 break 를 만나면 코드2는 실행하지 않고 while문이 종료된다.
while (조건식) {
코드1;
break; //즉시 while문 종료로 이동한다.
코드2;
} //while문 종료
아래 코드는 sum이 10보다 크면 break문을 통해 반복문을 탈출하는 코드이다.
public class Break1 {
public static void main(String[] args) {
int sum = 0;
int i = 1;
while (true) {
sum += i;
if (sum > 10) {
break;
System.out.println("합이 10보다 크면 종료: i=" + i + " sum=" + sum);
}
i++;
}
}
}
continue
continue를 만나면 코드2 가 실행되지 않고 다시 조건식으로 이동한다. 조건식이 참이면 while 문을 실행한다.
while (조건식) {
코드1;
continue; //즉시 조건식으로 이동한다.
코드2;
}
아래 코드는 i가 3일 때 continue을 통해 반복문의 조건식으로 이동하는 코드이다.
public class Continue1 {
public static void main(String[] args) {
int i = 1;
while (i <= 5) {
if (i == 3) {
i++;
continue;
}
System.out.println(i);
i++;
}
}
}
for문1
for문도 while문과 같은 반복문이고, 코드를 반복 실행하는 역할을 한다. for문은 주로 반복 횟수가 정해져 있을 때 사용한다. for문은 다음과 같은 순서대로 실행된다.
step 1
초기식이 실행된다. 주로 반복 횟수와 관련된 변수를 선언하고 초기화 할 때 사용한다.
초기식은 딱 1번 사용된다.
step 2
조건식을 검증한다. 참이면 코드를 실행하고, 거짓이면 for문을 빠져나간다.
step 3
코드를 실행한다.
step 4
코드가 종료되면 증감식을 실행한다. 주로 초기식에 넣은 반복 횟수와 관련된 변수의 값을 증가할 때 사용한다.
step 5
다시 2. 조건식 부터 시작한다. 무한 반복
for (1.초기식; 2.조건식; 4.증감식) {
// 3.코드
}
for문은 while문을 조금 더 편하게 다룰 수 있도록 구조화 한거라고 볼 수 있는데 다음과 같은 예제를 보면 조금 더 for문 사용법에 대해서 알 수 있다. 1부터 10까지의 값을 출력하는 반복문인데 for문의 구조 순서대로 따라가면 순차적으로 1부터 10까지의 값을 출력하는 것을 알 수 있다.
public class For1 {
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
System.out.println(i);
}
}
}
for문2
for문에서 초기식, 조건식, 증감식은 선택이다.
for (초기식; 조건식; 증감식) {
// 코드
}
다음과 같이 모두 생략해도 된다. 단 생략해도 각 영역을 구분하는 세미콜론(;)은 유지해야한다
for (;;) {
// 코드
}
결국 위 반복문은 다음과 같은 코드가 된다.
while (true) {
// 코드
}
위와같은 초기식 조건식 증감식을 전부 생략하고 사용하는 코드는 어떤 상황에서 사용할까 ? 솔직히 좀 억지스럽지만 이렇게도 사용할 수 있다는 문법적인 내용으로만 이해하는게 좋은거 같다. 실제로는 거의 사용을 안한다.
아래 코드는 1부터 시작해서 숫자를 계속 누적해서 더하다가 10보다 큰 시점이 되었을떄의 i 값을 구하는 문제이다. 쉽게 얘기해서 i = 1일때 sum = 1, i = 2일때 sum = 3, i = 3일때 sum = 6, i = 4일때 sum = 10, i = 5일때는 sum = 15 가 되기에 10보다 큰 값이 처음으로 나오는 i 값인 5를 출력하면 되는 것이다. 다만 이것을 초기식 조건식 증감식을 생략한 for문으로 코드를 구현 해 보는 것이다.
public class Break2 {
public static void main(String[] args) {
int sum = 0;
int i = 1;
for (; ; ) { // 초기식 조건식 증감식 생략
sum += i;
if (sum > 10) {
break; // if 조건이 만족하였을때 반복문 탈출
System.out.println("합이 10보다 크면 종료: i=" + i + " sum=" + sum);
}
i++;
}
}
}
중첩 반복문
반복문은 내부에 또 반복문을 만들 수 있다. for, while 모두 가능하다.
public class Nested1 {
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
System.out.println("외부 for 시작 i:" + i);
for (int j = 0; j < 3; j++) {
System.out.println("-> 내부 for " + i + "-" + j);
}
System.out.println("외부 for 종료 i:" + i);
System.out.println(); //라인 구분을 위해 실행
}
}
}
다음은 위 코드의 실행 결과이며, 외부 for는 2번, 내부 for는 3번 실행된다. 그런데 외부 for 1번당 내부 for가 3번 실행되기 때문에 외부(2) * 내부(3) 해서 총 6번의 내부 for 코드가 수행된다. 이러한 중첩 반복문은 평소에도 자주 사용되고 실제 개발 환경 및 알고리즘 문제에서도 굉장히 자주 사용이 된다. 정말 많이 사용한다.
외부 for 시작 i:0
-> 내부 for 0-0
-> 내부 for 0-1
-> 내부 for 0-2
외부 for 종료 i:0
외부 for 시작 i:1
-> 내부 for 1-0
-> 내부 for 1-1
-> 내부 for 1-2
외부 for 종료 i:1
-
[design-pattern] strategy 패턴
intro : strategy 패턴에 대해서 알아보자
Strategy 패턴이란 ?
Strategy 패턴은 특정 작업을 수행하는 여러 방식(즉, 전략)을 정의하고, 필요에 따라 이를 교체하며 사용할 수 있도록 설계된 행동 패턴입니다. 쉽게 말해, 어떤 작업을 처리하는 방식(모드)을 각각의 클래스로 분리하고, 실행 시점에 적절한 전략을 선택해 사용하는 것입니다. 예를 들어, 다이소에서 물건을 결제할 때 다양한 결제 방식을 선택할 수 있는 화면이 나온다고 가정해 보겠습니다. 사용할 수 있는 결제 방식으로는 카드 결제, 현금 결제, 카카오페이 결제, 애플페이 결제, 네이버페이 결제 등이 있을 수 있습니다. 만약 이러한 결제 프로그램을 우리가 작성할 때, 결제라는 행위는 동일하지만, 선택된 결제 방식에 따라 수행되어야 하는 로직이 달라지게 됩니다. 이처럼 다양한 결제 방식을 유연하게 처리하기 위해 Strategy 패턴을 적용할 수 있습니다. Strategy 패턴을 통해 각 결제 방식을 별도 클래스로 분리하고, 실행 시점에 적절한 전략을 선택하여 사용함으로써 코드의 확장성과 유연성을 높일 수 있습니다.
Strategy 패턴은 언제 사용하는가 ?
Strategy 패턴은 행동(Behavior)을 캡슐화하고, 다양한 알고리즘(전략)을 동적으로 교체할 수 있도록 설계할 때 사용한다. 이 패턴은 특정 작업을 수행하는 여러 알고리즘(전략)들이 존재하며, 이들 사이를 유연하게 전환하거나 교체해야 할 때 유용하다.
Strategy 패턴 적용 코드 (1)
결제 전략에 사용할 인터페이스
// 결제 방식에 대한 인터페이스
interface PaymentStrategy {
void pay(int amount);
}
결제 전략 인터페이스를 구현한 실제 전략 클래스 (구현체)
// PaymentStrategy 인터페이스를 구현한 신용카드 결제 방식 클래스 구성
class CreditCardPayment implements PaymentStrategy {
private String name;
private String cardNumber;
public CreditCardPayment(String name, String cardNumber) {
this.name = name;
this.cardNumber = cardNumber;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid with credit card");
}
}
// PaymentStrategy 인터페이스를 구현한 페이팔 결제 방식 클래스 구성
class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid using PayPal");
}
}
각 전략(신용카드결제, 페이팔결제) 에 맞게 결제 메소드를 호출할수 있는 클래스
// CreditCardPayment, PayPalPayment 클래스를 통해 객체를 생성할 수 있는 ShoppingCart 클래스
// setPaymentStrategy 메소드를 통해 어떤 전략이던지 동적으로 선택할 수 있다.
// 해당 클래스의 checkout 메소드를 통해 각 전략에 맞는 결제 메소드를 실행 할 수 있다.
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
클라이언트 코드 에서 ShoppingCart 클래스 setPaymentStrategy 메소드를 통해 신용카드 결제 방식으로 결제, 두번째로는 setPaymentStrategy 메소드를 통해 페이팔 결제방식 으로 전환. 다음과 같은 코드를 통해 각 상황에 맞게 전략을 동적으로 선택 할 수 있음을 알 수 있다.
// Client code
public class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(
new CreditCardPayment(
"John Doe",
"1234567890123456"
)
);
//100 paid with credit card
cart.checkout(100);
cart.setPaymentStrategy(
new PayPalPayment(
"johndoe@example.com"
)
);
// 200 paid using PayPal
cart.checkout(200);
}
}
Strategy 패턴 적용 코드 (2)
문자열을 압축하는 프로그램을 구성할건데, 압축하는 방식을 두가지 전략으로 구성하고자 한다. 1번은 각 문자가 연속되는 횟수를 기록하여 문자열을 압축하는 방식, 2번은 문자열의 각 모음을 정해진 숫자로 변환하는 방식이 있다. 해당 상황을 가정하고 아래와 같은 코드를 따라가보자.
압축 전략에 사용할 인터페이스
interface CompressionStrategy {
String compress(String data);
}
압축 전략 인터페이스를 구현한 실제 전략 클래스 (구현체)
class RunLengthEncoding implements CompressionStrategy {
@Override
public String compress(String data) {
StringBuilder compressed = new StringBuilder();
int count = 1;
for (int i = 1; i <= data.length(); i++) {
if (i < data.length() && data.charAt(i) == data.charAt(i - 1)) {
count++;
} else {
compressed.append(data.charAt(i - 1));
compressed.append(count);
count = 1;
}
}
return compressed.toString();
}
}
class SimpleReplacementCompression implements CompressionStrategy {
@Override
public String compress(String data) {
return data.replace("a", "1")
.replace("e", "2")
.replace("i", "3")
.replace("o", "4")
.replace("u", "5");
}
}
각 전략(RunLengthEncoding, SimpleReplacementCompression) 에 맞게 압축 메소드를 호출할 수 있는 클래스
// Context class
class Compressor {
private CompressionStrategy strategy;
public void setCompressionStrategy(CompressionStrategy strategy) {
this.strategy = strategy;
}
public String compress(String data) {
return strategy.compress(data);
}
}
클라이언트 코드 에서 Compressor 클래스 setCompressionStrategy 메소드를 통해 RunLengthEncoding방식으로 압축, 두번째로는 setCompressionStrategy메소드를 통해 SimpleReplacementCompression 방식으로 전환. 결국 1번에서 확인했던 전략 패턴의 큰 틀의 구조와 같다고 볼 수 있다. 전략패턴을 적용할 상황만 살짝 다를 뿐이었다.
// Client code
public class Main {
public static void main(String[] args) {
Compressor compressor = new Compressor();
String data = "aabcccccaaa";
compressor.setCompressionStrategy(new RunLengthEncoding());
System.out.println(
"RLE Compression: " + compressor.compress(data)
);
// RLE Compression: a2b1c5a3
compressor.setCompressionStrategy(new SimpleReplacementCompression());
System.out.println(
"Simple Replacement: " + compressor.compress(data)
);
// Simple Replacement: 11bccccc111
}
}
Strategy 패턴 요약
Strategy 패턴은 동일한 작업을 수행하는 여러 방법을 정의하고, 실행 시 적합한 방법을 동적으로 선택하여 사용하는 행동 패턴이다.
-
[jpa] 영속성 관리(내부 동작 방식)
intro : jpa의 영속성 관리에 대해 알아보자.
JPA에서 가장 중요한 2가지
1. 객체와 관계형 데이터베이스 매핑하기
2. 영속성 컨텍스트
엔티티 매니저 팩토리와 엔티티 매니저
EntityManagerFactory
엔티티 매니저 팩토리는 JPA 애플리케이션에서 데이터베이스와의 연결을 관리하고 엔티티 매니저 객체를 생성하는 팩토리입니다. 데이터베이스와의 연결 설정, 캐싱, 그리고 여러 개의 엔티티 매니저 인스턴스를 관리하는데 사용됩니다.
EntityManager
엔티티 매니저는 JPA의 핵심 인터페이스로, 엔티티를 데이터베이스에 CRUD(Create, Read, Update, Delete) 작업을 수행하는 데 사용됩니다. 엔티티 매니저 특정 단위의 작업(예: 하나의 트랜잭션 또는 요청)을 처리할 때 사용됩니다.
영속성 컨텍스트
JPA 를 이해하는데 가장 중요한 용어, 엔티티를 영구 저장하는 환경 이라는 뜻. 영속성 컨텍스트는 논리적인 개념이며 엔티티 매니저를 통해서 영속성 컨텍스트에 접근 가능.
엔티티의 생명주기
비영속
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
Member member = new Member();
영속
영속성 컨텍스트에 관리되는 상태
em.persist(member);
준영속
영속성 컨텍스트에 저장되었다가 분리된 상태
em.detach(member);
em.clear();
em.close();
삭제
삭제된 상태
em.remove(member)
영속성 컨텍스트의 이점
1차 캐시
엔티티 매니저는 자체적으로 1차 캐시를 갖고 있습니다. 같은 엔티티 매니저 내에서 동일한 엔티티를 여러 번 조회하면, 데이터베이스에 다시 쿼리를 보내지 않고 1차 캐시에서 반환합니다.
동일성 보장
동일한 엔티티 매니저 내에서 같은 엔티티를 여러 번 조회하면 동일한 객체 인스턴스가 반환됩니다.
트랜잭션을 지원하는 쓰기 지연
엔티티 매니저는 persist()나 merge() 등으로 데이터를 수정해도 즉시 데이터베이스에 반영하지 않고, 트랜잭션이 커밋될 때 한꺼번에 SQL 쿼리를 보냅니다.
변경 감지
엔티티 매니저는 엔티티의 변경 사항을 자동으로 감지하고, 트랜잭션이 커밋될 때 변경된 내용을 데이터베이스에 반영합니다. (스냅샷과 비교하여 변경을 감지함)
지연 로딩
엔티티의 연관된 데이터가 실제로 필요할 때까지 데이터베이스 조회를 지연시킵니다. 위 특징으로 인해 불필요한 데이터 로드를 피하고 성능을 최적화할 수 있습니다.
플러시
영속성 컨텍스트의 변경 내용을 데이터 베이스에 반영. 이때 영속성 컨텍스트를 비우지않음. 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화. 트랜잭션이라는 작업단위가 중요하며, 커밋 직전에만 동기화 하면 됨.
영속성 컨텍스트를 플러시 하는 방법
em.flush() 직접 호출
트랜잭션 커밋 플러시 자동 호출
JPQL 쿼리 실행 플러시 자동 호출
JPQL 쿼리 실행시 플러시가 자동으로 호출되는 이유
JPQL 실행 시 자동 플러시는 쿼리가 실행되기 전에 EntityManager의 변경 사항이 반영되도록 하여, JPQL의 결과가 데이터의 최신 상태를 보장하도록 돕는 중요한 동작입니다.
-
[java] 조건문
intro : java 조건문에 대한 개념을 알아보자.
if문1 - if else
조건문 시작
특정 조건에 따라서 코드를 실행하려면 어떻게 해야할까? 예를들어서 만약 18살 이상이면 “성인입니다.” 를 출력하고, 만약 18살 미만이라면 “미성년자입니다.”를 출력해야 한다면? 아마도 다음과 같이 코딩을 해야 할 것이다.
// 한글
만약 (나이 >= 18)면 "성인입니다"
만약 (나이 < 18)면 "미성년자입니다"
// 영어
if (age >= 18) "성인입니다"
if (age < 18) "미성년자입니다"
이렇게 특정 조건에 따라서 코드를 실행하는 것을 조건문 이라고 한다. 조건문에는 if문 switch문이 있다. 둘다 특정 조건에 따라서 다른 코드를 실행하는 것이라 생각하면 된다.
if 문
if문은 특정 조건이 참인지 확인하고 그 조건이 참(true)일 경우 특정 코드 블록을 실행한다. 아래 코드에서는 첫번째 조건문인 if문은 참 이기에 실행이되고, 두번째 조건문에서는 거짓 이기에 실행되지 않는다.
public class If1 {
public static void main(String[] args) {
int age = 20; // 사용자 나이
if (age >= 18) { // 참
System.out.println("성인입니다.");
}
if (age < 18) { // 거짓
System.out.println("미성년자입니다.");
}
}
}
else 문
else 문은 if문에서 만족하는 조건이 없을 때 실행하는 코드를 제공한다.
if (condition) {
// 조건이 참일 때 실행되는 코드
} else {
// 만족하는 조건이 없을 때 실행되는 코드
}
아래 코드에서 사용자의 나이가 10이라면, 첫번째 if문을 만족하지 않기에, else문의 “미성년자입니다.” 문구가 출력된다.
public class If2 {
public static void main(String[] args) {
int age = 10; // 사용자의 나이
if (age >= 18) {
System.out.println("성인입니다."); //참일 때 실행
} else {
System.out.println("미성년자입니다.");//만족하는 조건이 없을 때 실행
}
}
}
if문2 - else if
else if 문은 앞선 if문의 조건이 거짓일 때 다음 조건을 검사한다. 만약 앞선 if문이 참이라면 else if 문을 실행하지 않는다. 여러개의 if문을 하나로 묶을때 이렇게 if-else문을 사용할 수 있다. 이렇게 하면 특정 조건이 만족하면 해당 코드를 실행하고 if문 전체를 빠져나온다. 특정 조건을 만족하지 않으면 다음 조건을 검사한다. 여기서 핵심은 순서대로 맞는 조건을 찾아보고 맞는 조건이 있으면 딱1개만 실행이 되는 것이다.
if (condition1) {
// 조건1이 참일 때 실행되는 코드
} else if (condition2) {
// 조건1이 거짓이고, 조건2가 참일 때 실행되는 코드
} else if (condition3) {
// 조건2이 거짓이고, 조건3이 참일 때 실행되는 코드
} else {
// 모든 조건이 거짓일 때 실행되는 코드
}
아래 코드를 보면 더 와닿게 else-if 문을 이해할 수 있다. age = 7인 경우 if(age <= 7) 의 조건이 참이다. “미취학”을 출력하고 전체 if 문 밖으로 나간다. age = 13인 경우 if(age <= 7) 의 조건이 거짓이다. 다음 조건으로 넘어간다. else if(age <= 13) 의 조건이 참이다. “초등학생”을 출력하고 전체 if 문 밖으로 나간다. **age = 50인 경우 if(age <= 7) 의 조건이 거짓이다. 다음 조건으로 넘어간다. else if(age <= 13) 의 조건이 거짓이다. 다음 조건으로 넘어간다.else if(age <= 16) 의 조건이 거짓이다. 다음 조건으로 넘어간다. else if(age <= 19) 의 조건이 거짓이다. 다음 조건으로 넘어간다. else 만족하는 조건 없이 else 까지 왔다. else 에 있는 “성인”을 출력하고 전체 if 문 밖으로 나간다.
public class If4 {
public static void main(String[] args) {
int age = 14;
if(age <= 7) { //~7: 미취학
System.out.println("미취학");
} else if(age <= 13) { //8~13: 초등학생
System.out.println("초등학생");
} else if(age <= 16) { //14~16: 중학생
System.out.println("중학생");
} else if(age <= 19) { //17~19: 고등학생
System.out.println("고등학생");
} else { //20~: 성인
System.out.println("성인");
}
}
}
if문3 - if 문과 else if문
if문에 else-if문을 함께 사용하는 것은 서로 연관된 조건일 때 사용한다. 그런데 서로 관련이 없는 독립 조건이면, else-if를 사용하지 않고 if문을 각각 따로 사용해야 한다.
아래와 같은 상황에서 if문을 독립적으로 사용한다. (else-if X)
if문을 독립적으로 적용하는 상황
온라인 쇼핑몰의 할인 시스템을 개발해야 한다. 한 사용자가 어떤 상품을 구매할 때, 다양한 할인 조건에 따라 총 할인 금액이 달라질 수 있다. 각각의 할인 조건은 다음과 같다. 아이템 가격이 10000원 이상일 때, 1000원 할인 나이가 10살 이하일 때 1000원 할인 이 할인 시스템의 핵심은 한 사용자가 동시에 여러 할인을 받을 수 있다는 점이다. 예를 들어, 10000원짜리 아이템을 구매할 때 1000원 할인을 받고, 동시에 나이가 10살 이하이면 추가로 1000원 더 할인을 받는다. 그래서 총 2000원 까지 할인을 받을 수 있다.
public class If5 {
public static void main(String[] args) {
int price = 10000;// 아이템 가격
int age = 10;//나이
int discount = 0;
if (price >= 10000) {
discount = discount + 1000;
System.out.println("10000원 이상 구매, 1000원 할인");
}
if (age <= 10) {
discount = discount + 1000;
System.out.println("어린이 1000원 할인");
}
System.out.println("총 할인 금액: " + discount + "원");
}
}
if문 - switch
switch 문은 앞서 배운 if문을 조금 더 편리하게 사용할 수 있는 기능이다. 참고로 if문은 비교연산자를 사용할 수 있지만, switch문은 단순히 값이 같은지만 비교할 수 있다. switch문은 조건식에 해딩하는 특정 값으로 실행할 코드를 선택한다. 조건식의 결과 값이 어떤 case 의 값과 일치하면 해당 case 의 코드를 실행한다. break 문은 현재 실행 중인 코드를 끝내고 switch 문을 빠져나가게 하는 역할을 한다. 만약 break 문이 없으면, 일치하는 case 이후의 모든 case 코드들이 순서대로 실행된다. default 는 조건식의 결과값이 모든 case 의 값과 일치하지 않을 때 실행된다. if 문의 else 와 같다. default 구문은 선택이다. if , else-if , else 구조와 동일하다.
public class Main {
public static void main(String[] args) {
switch (조건식) {
case value1:
// 조건식의 결과 값이 value1일 때 실행되는 코드
break;
case value2:
// 조건식의 결과 값이 value2일 때 실행되는 코드
break;
default:
// 조건식의 결과 값이 위의 어떤 값에도 해당하지 않을 때 실행되는 코드
}
}
}
실제 switch 문을 사용한 예제 코드를 살펴보자 변수 grade 값에 따라 실행되는 case문이 달라진다.
public class Switch2 {
public static void main(String[] args) {
//grade 1:1000, 2:2000, 3:3000, 나머지: 500
int grade = 2;
int coupon;
switch (grade) {
case 1:
coupon = 1000;
break;
case 2:
coupon = 2000;
break;
case 3:
coupon = 3000;
break;
default:
coupon = 500;
}
System.out.println("발급받은 쿠폰 " + coupon);
}
}
case문에 break 문이 없으면?
만약 break문이 없으면 어떻게 되는지 확인하기 위해 다음과 같이 조건을 변경해보자. 2등급과 3등급이 같이 3000원 쿠폰을 발급한다고 가정해보겠다. case2에는 break문이 없다. 그러면 중단하지 않고, 바로 다음에 있는 case3의 코드를 실행한다. 여기서 coupon = 3000을 수행하고, break문을 만나서 switch문 밖으로 빠져나간다. 발급받은 쿠폰 3000이 출력된다.
public class Switch3 {
public static void main(String[] args) {
//grade 1:1000, 2:3000(변경), 3:3000, 나머지: 500
int grade = 2;
int coupon;
switch (grade) {
case 1:
coupon = 1000;
break;
case 2:
case 3:
coupon = 3000;
break;
default:
coupon = 500;
break;
}
System.out.println("발급받은 쿠폰 " + coupon);
}
}
if문 - 삼항 연산자
if문을 사용할 때 다음과 같이 단순히 참과 거짓에 따라 특정 값을 구하는 경우가 있다. 삼항 연산자 없이 if 문만 사용해도 된다. 하지만 단순히 참과 거짓에 따라서 특정 값을 구하는 삼항 연산자를 사용하면 if 문 보다 간결한 코드를 작성할 수 있다
public class CondOp2 {
public static void main(String[] args) {
int age = 18;
String status = (age >= 18) ? "성인" : "미성년자"; // if문을 사용할 때 보다 간결한 코드 구성 가능
System.out.println("age = " + age + " status = " + status);
}
}
-
[java] 연산자
intro : java 연산자에 대한 개념을 알아보자.
연산자
자바에는 산술연산자, 증감 연산자, 비교 연산자, 논리 연산자, 대입 연산자, 삼항 연산자 등과 같은 다양한 연산자가 존재한다. 기본적으로 연산자와 피연산자의 개념은 다음과 같다.
3 + 4 // 연산자(operator): 연산 기호 - `+`, `-`
a + b // 피연산자(operand): 연산 대상 - `3`, `4`, `a`, `b`
산술 연산자
산술 연산자는 주로 숫자를 계산하는 데 사용된다. 우리가 이미 잘 알고 있는 수학 연산을 수행한다. 산술연산자의 종류는 더하기 (+) 빼기 (-) 곱하기(*) 나누기(/) 나머지(%) 가 있다.
public class Operator1 {
public static void main(String[] args) {
// 변수 초기화
int a = 5;
int b = 2;
// 덧셈
int sum = a + b;
System.out.println("a + b = " + sum); // 출력: a + b = 7
// 뺄셈
int diff = a - b;
System.out.println("a - b = " + diff); // 출력: a - b = 3
// 곱셈
int multi = a * b;
System.out.println("a * b = " + multi); // 출력: a * b = 10
// 나눗셈
int div = a / b;
System.out.println("a / b = " + div); // 출력: a / b = 2
// 나머지
int mod = a % b;
System.out.println("a % b = " + mod); // 출력: a % b = 1
}
}
주의! 0 으로 나누기
10 / 0 과 같이 숫자는 0으로 나눌 수 없다. 이 경우 프로그램에 다음과 같은 오류가 발생한다. (Exception in thread "main" java.lang.ArithmeticException: / by zero)
문자열 더하기
자바는 특별하게도 문자열에도 + 연산자를 사용할 수 있다. 문자열에 + 연산자를 사용하면 두 문자를 연결 할 수 있다.
public class Operator2 {
public static void main(String[] args) {
//문자열과 문자열 더하기1
String result1 = "hello " + "world";
System.out.println(result1);
//문자열과 문자열 더하기2
String s1 = "string1";
String s2 = "string2";
String result2 = s1 + s2;
System.out.println(result2);
//문자열과 숫자 더하기1
String result3 = "a + b = " + 10;
System.out.println(result3);
//문자열과 숫자 더하기2
int num = 20;
String str = "a + b = ";
String result4 = str + num;
System.out.println(result4);
}
}
주의! 문자열 + 숫자
자바에서 문자와 숫자를 더하면 숫자를 문자열로 변경한 다음에 서로 더한다. 아래 코드의 결과는 20을 예상할 수도 있지만, 실제로는 1010 값이 출력된다. 쉽게 얘기해서 문자열과 더하는 모든 것들은 전부 문자열이 된다.
public class Main {
public static void main(String[] args) {
//문자열과 문자열 더하기1
String result1 = "hello " + "world";
System.out.println(result1);
String str = "10";
int a = 10;
System.out.println(str + a); // 1010 출력
}
}
연산자 우선순위
자바는 다음과 같은 연산자 우선순위가 있다. 높은 것에서 낮은 순으로 적용된다. 처음에 나오는 괄호가 우선순위가 가장 높고, 대입 연산자가 우선순위가 가장 낮다.
우선순위
연산자 유형
연산자
1
괄호
()
2
단항 연산자
++,--,!,~,new,(type)
3
산술 연산자
*, /, %, +, -
4
Shift 연산자
<<, >>, >>>
5
비교 연산자
<,<=,>,>=,instanceof
6
등식 연산자
==, !=
7
비트 연산자
&, ^, |
8
논리 연산자
&&, ||
9
삼항 연산자
? :
10
대입 연산자
=, +=, -=, *=, /=, %= 등
잘 모르는 ~ 연산자
자바에서 ~ 연산자는 비트 반전 연산자(bitwise NOT)입니다. 이 연산자는 피연산자의 모든 비트를 반전시키는 데 사용됩니다. 즉, 0을 1로, 1을 0으로 바꿉니다. 이걸 응용하면, 음수를 비트로 표현하는 방법에 대해서도 알 수 있습니다. -5를 표현하는 방법은 5의 보수 즉 ~5 을 한뒤 +1 을 해주는 것으로 음수를 2진법으로 표현할 수 있습니다.
public class Main {
public static void main(String[] args) {
int a = 5; // 이진수: 00000000 00000000 00000000 00000101
int result = ~a;
System.out.println(result); // 출력: -6
}
}
잘 모르는 Shift 연산자
1. << 왼쪽 쉬프트 연산자
비트를 왼쪽으로 이동시키고, 오른쪽에 0을 채웁니다.
예시: a << n은 a를 왼쪽으로 n비트만큼 이동시킵니다.
int a = 1; // 이진수: 00000001
int result = a << 1; // 왼쪽으로 1비트 이동
System.out.println(result); // 출력: 2 (이진수: 00000010)
2. >> 오른쪽 쉬프트 연산자
비트를 오른쪽으로 이동시키고, 왼쪽에 부호 비트를 채웁니다. (부호 비트는 양수인 경우 0, 음수인 경우 1)
a >> n은 a를 오른쪽으로 n비트만큼 이동시키며, 부호 비트(1 또는 0)로 채웁니다.
int a = 4; // 이진수: 00000100
int result = a >> 1; // 오른쪽으로 1비트 이동
System.out.println(result); // 출력: 2 (이진수: 00000010)
3. >>> 무조건 오른쪽 쉬프트 연산자 (<<< 무조건 왼쪽 쉬프트 연산자는 없습니다.)
비트를 오른쪽으로 이동시키고, 왼쪽에 항상 0을 채웁니다. 부호 비트와 상관없이 0으로 채워집니다.
예시: a >>> n은 a를 오른쪽으로 n비트만큼 이동시키며, 빈 자리는 항상 0으로 채웁니다.
int a = -5; // 이진수: 11111111111111111111111111111011
int result = a >>> 1; // 오른쪽으로 1비트 이동
System.out.println(result); // 출력: 2147483642
잘 모르는 비트 연산자
1. & 연산자
&는 비트 단위 AND 연산자로, 두 비트가 모두 1일 때만 결과가 1이 됩니다. 그 외의 경우에는 결과가 0이 됩니다.
public class Main {
public static void main(String[] args) {
int a = 5; // 이진수: 0101
int b = 3; // 이진수: 0011
int result = a & b;
System.out.println(result); // 출력: 1
}
}
2. ^ (비트 XOR) 연산자
^는 비트 단위 XOR 연산자로, 두 비트가 서로 다를 때만 결과가 1이 됩니다. 같은 비트는 0이 됩니다.
public class Main {
public static void main(String[] args) {
int a = 5; // 이진수: 0101
int b = 3; // 이진수: 0011
int result = a ^ b;
System.out.println(result); // 출력: 6
}
}
3. | (비트 OR) 연산자
|는 비트 단위 OR 연산자로, 두 비트 중 하나라도 1이면 결과가 1이 됩니다. 두 비트가 모두 0일 때만 결과가 0입니다.
public class Main {
public static void main(String[] args) {
int a = 5; // 이진수: 0101
int b = 3; // 이진수: 0011
int result = a | b;
System.out.println(result); // 출력: 7
}
}
증감 연산자
증가 및 감소 연산자를 줄여서 증감 연산자라 한다. 증감 연산자는 ++ 와 -- 로 표현되며, 이들은 변수의 값을 1만큼 증가시키거나 감소시킨다. 프로그래밍에서는 값을 1씩 증가하거나 1씩 감소할 때가 아주 많기 때문에 이런 편의 기능을 제공한다.
public class OperatorAdd1 {
public static void main(String[] args) {
int a = 0;
a = a + 1;
System.out.println("a = " + a); //1
a = a + 1;
System.out.println("a = " + a); //2
//증감 연산자
++a; //a = a + 1
System.out.println("a = " + a); //3
++a; //a = a + 1
System.out.println("a = " + a); //4
}
}
전위, 후위 증감연산자
증감 연산자는 피연산자 앞에 두거나 뒤에 둘 수 있으며, 연산자의 위치에 따라 연산이 수행되는 시점이 달라진다. ++a : 증감 연산자를 피연산자 앞에 둘 수 있다. 이것을 앞에 있다고 해서 전위(Prefix) 증감 연산자라 한다. a++ : 증감 연산자를 피연산자 뒤에 둘 수 있다. 이것을 뒤에 있다고 해서 후위(Postfix) 증감 연산자라 한다.
public class OperatorAdd2 {
public static void main(String[] args) {
// 전위 증감 연산자 사용 예
int a = 1;
int b = 0;
b = ++a; // a의 값을 먼저 증가시키고, 그 결과를 b에 대입
System.out.println("a = " + a + ", b = " + b); // 결과: a = 2, b = 2
// 후위 증감 연산자 사용 예
a = 1; // a 값을 다시 1로 지정
b = 0; // b 값을 다시 0으로 지정
b = a++; // a의 현재 값을 b에 먼저 대입하고, 그 후 a 값을 증가시킴
System.out.println("a = " + a + ", b = " + b); // 결과: a = 2, b = 1
}
}
비교 연산자
비교 연산자는 두 값을 비교하는 데 사용한다. 비교 연산자는 주로 뒤에서 설명하는 조건문과 함께 사용한다. 비교 연산자를 사용하면 참(true) 또는 거짓(false)이라는 결과가 나온다. 참 거짓은 boolean 형을 사용한다. 여기서 주의할 점은 = 와 == (= x2)이 다르다는 점이다.= : 대입 연산자, 변수에 값을 대입한다. == : 동등한지 확인하는 비교 연산자
public class Comp1 {
public static void main(String[] args) {
int a = 2;
int b = 3;
System.out.println(a == b); // false, a와 b는 같지 않다
System.out.println(a != b); // true, a와 b는 다르다
System.out.println(a > b); // false, a는 b보다 크지 않다
System.out.println(a < b); // true, a는 b보다 작다
System.out.println(a >= b); // false, a는 b보다 크거나 같지 않다
System.out.println(a <= b); // true, a는 b보다 작거나 같다
//결과를 boolean 변수에 담기
boolean result = a == b; //a == b: false
System.out.println(result); //false
}
}
문자열 비교
문자열 비교는 객체이기 때문에 == 으로 비교할수 없다. 문자열 String 클래스가 제공하는 equals 메소드를 통해 비교해야한다.
논리 연산자
논리 연산자는 boolean 형인 true , false 를 비교하는데 사용한다. && (그리고) : 두 피연산자가 모두 참이면 참을 반환, 둘중 하나라도 거짓이면 거짓을 반환 || (또는) : 두 피연산자 중 하나라도 참이면 참을 반환, 둘다 거짓이면 거짓을 반환 ! (부정) : 피연산자의 논리적 부정을 반환. 즉, 참이면 거짓을, 거짓이면 참을 반환
public class Logical1 {
public static void main(String[] args) {
System.out.println("&&: AND 연산");
System.out.println(true && true); //true
System.out.println(true && false);//false
System.out.println(false && false);//false
System.out.println("||: OR 연산");
System.out.println(true || true); //true
System.out.println(true || false);//true
System.out.println(false || false);//false
System.out.println("! 연산");
System.out.println(!true); //false
System.out.println(!false); //true
System.out.println("변수 활용");
boolean a = true;
boolean b = false;
System.out.println(a && b); // false
System.out.println(a || b); // true
System.out.println(!a); // false
System.out.println(!b); // true
}
}
대입 연산자
대입 연산자(=)는 값을 변수에 할당하는 연산자다. 이 연산자를 사용하면 변수에 값을 할당할 수 있다. 예를 들어, int a = 1 는 a 라는 변수에 1 이라는 값을 할당한다. 산술 연산자와 대입 연산자를 한번에 축약해서 사용할 수 있는데, 이것을 축약(복합) 대입 연산자라 한다. (연산자 종류: +=,-=,*=,/=,%=)
public class Assign1 {
public static void main(String[] args) {
int a = 5; // 5
a += 3; // 8 (5 + 3): a = a + 3
a -= 2; // 6 (8 - 2): a = a - 2
a *= 4; // 24 (6 * 4): a = a * 4
a /= 3; // 8 (24 / 3): a = a / 3
a %= 5; // 3 (8 % 5) : a = a % 5
System.out.println(a);
}
}
-
[design-pattern] facade 패턴
intro : facade 패턴에 대해서 알아보자
Facade 패턴이란 ?
Facade 란 프랑스어로 어떤 건물의 외벽을 뜻하는 말이다. 외벽을 뜻하는 단어로 이루어진 디자인 패턴은 어떤 패턴일까 ? 좀 더 자세히 알아보자면, 구조적 디자인 패턴 중 하나로 복잡한 서브시스템을 단순화하여 사용하는 패턴이라고 볼 수 있다. 이 패턴은 클라이언트가 복잡한 시스템이나 여러 클래스를 직접 다루지 않고, 단일 인터페이스를 통해 시스템을 간단히 사용할 수 있도록 해준다. 즉, 복잡한 내부 시스템을 숨기고, 간단하고 일관된 인터페이스를 제공하는 것이 Facade 패턴의 핵심이라고 볼 수 있다.
Facade 패턴은 언제 사용하는가 ?
Facade 패턴은 복잡한 서브시스템의 인터페이스를 단순화하여 클라이언트가 쉽게 접근할 수 있도록 할 때 사용한다. 이 패턴은 클라이언트와 복잡한 서브시스템 간에 하나의 통합된 진입점을 제공함이 중요한 특징이다.
Facade 패턴 적용 전 (1)
어떠한 클라이언트는 아침에 일어나면 온도를 조절하고, 불을 켜고, 커피를 마신다. 이러한 행동은 다음과 같이 각 클래스의 메소드들로 나누어져 있으며, 객체를 생성하여 각각 호출하여야 한다.
public class Thermostat {
public void setTemperature(int temperature) {
System.out.println("Setting thermostat to " + temperature + " degrees.");
}
}
public class Lights {
public void on() {
System.out.println("Lights are on.");
}
public void off() {
System.out.println("Lights are off.");
}
}
public class CoffeeMaker {
public void brewCoffee() {
System.out.println("Brewing coffee.");
}
}
실제로 클라이언트는 각 클래스의 메소드들을 어떻게 호출해야 하는지 다음과 같은 코드를 보면 알 수 있다.
// Client code
public class Main {
public static void main(String[] args) {
Thermostat thermostat = new Thermostat(); // 온도 class 객체 생성
Lights lights = new Lights(); // 불 class 객체 생성
CoffeeMaker coffeeMaker = new CoffeeMaker(); // 커피 머신 class 객체 생성
thermostat.setTemperature(25); // 온도 25도 설정
lights.on(); // 불 전원 on
coffeeMaker.brewCoffee(); // 커미 끓이기
}
}
Facade 패턴 적용 후 (1)
기존에 각 클래스들로 나누어져 있던 것들을 하나로 모은 클래스 SmartHomeFacade 생성하여 클라이언트가 wakeUp, leaveHome 메소드를 통해 위 3개의 클래스를 제어 할 수 있도록 기능을 제공한다.
public class SmartHomeFacade {
private Thermostat thermostat;
private Lights lights;
private CoffeeMaker coffeeMaker;
public SmartHomeFacade(Thermostat thermostat, Lights lights, CoffeeMaker coffeeMaker) {
this.thermostat = thermostat;
this.lights = lights;
this.coffeeMaker = coffeeMaker;
}
public void wakeUp() {
System.out.println("Waking up...");
thermostat.setTemperature(22);
lights.on();
coffeeMaker.brewCoffee();
}
public void leaveHome() {
System.out.println("Leaving home...");
thermostat.setTemperature(18);
lights.off();
}
}
클라이언트는 아래 코드에서 보듯이 Thermostat Lights CoffeeMaker 클래스들의 각 역할을 알지 못하여도, 일관된 인터페이스로 제공된 SmartHomeFacade 객체의 wakeUp, leaveHome 메소드들을 통해 쉽고 간단하게 제어 할 수 있다. 이게 Facade 패턴의 장점이다.
// Client code
public class Main {
public static void main(String[] args) {
Thermostat thermostat = new Thermostat();
Lights lights = new Lights();
CoffeeMaker coffeeMaker = new CoffeeMaker();
SmartHomeFacade smartHome = new SmartHomeFacade(thermostat, lights, coffeeMaker);
smartHome.wakeUp();
smartHome.leaveHome();
}
}
Facade 패턴 적용 전 (2)
다른 예제로도 패턴 적용을 해보도록 하겠다. 다음과 같이 각 클래스가 FileReader, FileWriter, FileDeleter 3개로 나누어져 있다고 상황을 가정해 보자.
class FileReader {
public String readFile(String filePath)
throws IOException {
return new String(Files.readAllBytes(Paths.get(filePath)));
}
}
class FileWriter {
public void writeFile(String filePath, String content)
throws IOException {
Files.write(Paths.get(filePath), content.getBytes());
}
}
class FileDeleter {
public void deleteFile(String filePath)
throws IOException {
Files.delete(Paths.get(filePath));
}
}
이걸 만약에 클라이언트에서 각각의 클래스를 생성해서 사용하려면? 다음 코드와 같이 객체를 따로 생성해서 메소드를 각각의 클래스에 맞게 상황에 따로 호출해줘야 한다.
// Client code
public class Main {
public static void main(String[] args) throws IOException {
FileReader fileReader = new FileReader();
FileWriter fileWriter = new FileWriter();
FileDeleter fileDeleter = new FileDeleter();
String readFile = fileReader.readFile("test.txt");
fileWriter.writeFile("test.txt", "쓰고싶은 문자열");
fileDeleter.deleteFile("test.txt");
}
}
Facade 패턴 적용 후 (2)
Facade 패턴을 적용하면 내부 시스템의 클래스 객체를 생성하는 것 까지도 다음과 같이 처리 할 수 있다. 해당 객체를 생성하는 시점에서 필요한 서브시스템의 클래스들을 생성자에서 생성해버린다.
class FileSystemFacade {
private FileReader fileReader;
private FileWriter fileWriter;
private FileDeleter fileDeleter;
public FileSystemFacade() {
this.fileReader = new FileReader();
this.fileWriter = new FileWriter();
this.fileDeleter = new FileDeleter();
}
public String readFile(String filePath) {
try {
return fileReader.readFile(filePath);
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
return null;
}
}
public boolean writeFile(String filePath, String content) {
try {
fileWriter.writeFile(filePath, content);
return true;
} catch (IOException e) {
System.err.println("Error writing file: " + e.getMessage());
return false;
}
}
public boolean deleteFile(String filePath) {
try {
fileDeleter.deleteFile(filePath);
return true;
} catch (IOException e) {
System.err.println("Error deleting file: " + e.getMessage());
return false;
}
}
}
클라이언트는 아래 코드에서 보듯이 FileSystemFacade 클래스의 객체만 생성하며 서브시스템의 복잡도는 줄이고 높은 편의성을 제공하는 것을 알 수 있다.
// Client code
public class Main {
public static void main(String[] args) {
FileSystemFacade fs = new FileSystemFacade();
// Write to file
boolean writeSuccess = fs.writeFile("test.txt", "Hello, Facade Pattern!");
System.out.println("File write success: " + writeSuccess);
// Read from file
String content = fs.readFile("test.txt");
System.out.println("File content: " + content);
// Delete file
boolean deleteSuccess = fs.deleteFile("test.txt");
System.out.println("File delete success: " + deleteSuccess);
}
}
Facade 패턴 요약
Facade 패턴은 복잡한 시스템을 단순화하고, 내부 시스템에 대한 의존성을 줄이며, 클라이언트가 쉽게 사용할 수 있도록 편의성을 제공 해주는 구조적 디자인 패턴이다.
-
[java] 변수
intro : java 변수의 대한 개념을 알아보자.
변수 시작
변수란 무엇일까 ?
변수는 이름 그대로 변할 수 있는 값을 뜻한다. 숫자 정수를 보관할 수 있는 이름이 a라는 데이터 저장소를 만든다. 이것을 변수라고 한다. 이렇게 변수를 만드는 것을 변수 선언이라고 한다. 이제 변수 a에는 숫자 정수를 보관 할 수 있다. 숫자 정수 뿐만 아니라 문자, 소수와 같이 다양한 종류 값을 저장할 수 있는 변수들이 있다.
public class Main {
public static void main(String[] args) {
int a; //변수 선언
a = 10; //변수 a에 10 저장 및 초기화
System.out.println(a);
System.out.println(a);
System.out.println(a);
}
}
자바에서 = 은 오른쪽에 있는 값을 왼쪽으로 저장한다는 뜻이다. 수학에서 이야기하는 두 값이 같다 (equal) 와는 다른뜻이다. 숫자를 보관할 수 있는 데이터 저장소인 변수 a에 값 10을 저장한다. 이처럼 선언한 변수에 처음으로 값을 대입해서 저장하는 것을 변수 초기화 라고 한다.
변수 값 변경
변수는 이름 그대로 변할 수 있는 수이다. 쉽게 이야기해서 변수 a에 저장된 값을 언제든지 바꿀 수 있다는 뜼이다. 이번에는 중간에 변수의 값을 변경해보자.
public class Main {
public static void main(String[] args) {
int a; //변수 선언
a = 10; //변수 초기화: a(10)
System.out.println(a); //10 출력
a = 50; //변수 값 변경: a(10 -> 50)
System.out.println(a); //50 출력
}
}
변수 선언과 초기화
변수 선언
변수를 선언하면 컴퓨터의 메모리 공간을 확보해서 그곳에 데이터를 저장할 수 있다. 그리고 변수의 이름을 통해서 해당 메모리 공간에 접근 할 수 잇다. 정리하자면, 데이터를 보관할 수 있는 공간을 만들고, 그곳에 이름을 부여한다 라고 정리 할 수 있다. 변수는 다음과 같이 하나씩 선언할 수 있고, 한번에 여러 변수를 선언 할 수도 있다.
public class Main {
public static void main(String[] args) {
int a;
int b;
int c, d;
}
}
변수 초기화
변수를 선언하고, 선언한 변수에 처음으로 값을 저장하는 것을 변수 초기화 라고 한다. 변수를 초기화 하지 않고 사용하면 오류가 발생한다. 지역변수는 꼭 개발자가 직접 초기화를 해주어야한다. (나중에 배울 클래스 변수와 인스턴스 변수는 자바가 자동으로 초기화를 진행해준다)
public class Main {
public static void main(String[] args) {
//1. 변수 선언, 초기화 각각 따로, 지역변수이기 때문에 반드시 초기화 해주어야 함
int a;
a = 1;
System.out.println(a);
int b = 2; //2. 변수 선언과 초기화를 한번에
System.out.println(b);
int c = 3, d = 4; //3. 여러 변수 선언과 초기화를 한번에
System.out.println(c);
System.out.println(d);
}
}
변수 타입1
변수는 데이터를 다루는 종류에 따라 다양한 형식이 존재한다. 이러한 형식으로는 타입이라고 하고, 우리말로는 형식 또는 형 이라고 한다. 예를들어서 int 타입, int 형, int 형식 등으로 부른다. int는 정수를 다루며, double은 실수를 다룬다. boolean 불리언 타입은 true false 값만 사용할 수 있으며 거짓 참만 판단하는 곳에서 사용한다. char는 문자 하나를 다룰때 사용하며 작은 따옴표를 사용해서 감싸야한다. String은 문자열을 다루며 큰 따옴표를 사용하여 감싸야한다. (String은 첫글자가 대문자로 시작하는 특별한 타입이다.)
public class Main {
public static void main(String[] args) {
int a = 100; //정수
double b = 10.5; //실수
boolean c = true; //불리언(boolean) true, false 입력 가능
char d = 'A'; //문자 하나
String e = "Hello Java"; //문자열, 문자열을 다루기 위한 특별한 타입
System.out.println(a);
System.out.println(b);
System.out.println(c);
System.out.println(d);
System.out.println(e);
}
}
리터럴
코드에서 개발자가 직접적은 100, 10.5, true, 'A', "Hello Java"와 같은 고정된 값을 프로그래밍 용어 리터럴 이라고 한다. 변수의 값은 변할 수 있지만 리터럴은 개발자가 직접 입력한 고정된 값이다. 따라서 리터럴 자체는 변하지 않는다.
public class Main {
public static void main(String[] args) {
int a = 100; //정수 리터럴
double b = 10.5; //실수 리터럴
boolean c = true; //불리언 리터럴
char d = 'A'; //문자 하나 리터럴
String e = "Hello Java"; //문자열 리터럴
}
}
변수 타입2
메모리를 적게 사용하면 적은 숫자를 표현 할 수 있고, 메모리를 많이 사용하면 큰 숫자를 표현할 수 있다. 변수를 선언하면 표현범위에 따라 메모리 공간을 차지한다. 그래서 필요에 맞도록 다양한 타입을 제공한다.
정수형
byte : -128 ~ 127 (1byte, 2⁸)
short : -32,768 ~ 32,767 (2byte, 2¹⁶)
int : -2,147,483,648 ~ 2,147,483,647 (약 20억) (4byte, 2³²)
long : -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 (8byte, 2⁶⁴)
실수형
float : 대략 -3.4E38 ~ 3.4E38, 7자리 정밀도 (4byte, 2³²)
double : 대략 -1.7E308 ~ 1.7E308, 15자리 정밀도 (8byte, 2⁶⁴)
기타
boolean : true , false (1byte)
char : 문자 하나(2byte)
리터럴 타입 지정
정수 리터럴은 int을 기본으로 사용한다. 따라서 int 범위까지 표현 할 수 있다. 숫자가 int 범위인 약 20억을 넘어가면 L을 붙여서 정수 리터럴을 long으로 변경해야 한다. 실수 리터럴은 기본이 double 형을 사용한다. float형을 사용하려면 f을 붙여서 float 형으로 지정해야 한다.
자주 사용하는 변수 타입
정수 - int , long : 자바는 정수에 기본으로 int 를 사용한다. 만약 20억이 넘을 것 같으면 long 을 쓰면 된다. 파일을 다룰 때는 byte 를 사용한다.
실수 - double : 실수는 고민하지 말고 double 을 쓰면 된다.
불린형 - boolean : true , false 참 거짓을 표현한다. 이후 조건문에서 자주 사용된다.
문자열 - String : 문자를 다룰 때는 문자 하나든 문자열이든 모두 String 을 사용하는 것이 편리하다.
변수 범위를 구하는 공식
2^(n-1) ~ 2^(n-1) -1
char 타입은 예외로 0 ~ 2^n -1
변수 명명 규칙
자바에서 변수의 이름을 짓는데는 규칙과 관례가 있다. 규칙은 필수이다. 규칙을 지키지 않으면 컴파일 오류가 발생한다. 관례는 필수가 아니지만 전세계 개발자가 해당 관례를 따르기 때문에 사실상 규칙이라고 생각해도 된다.
규칙
변수 이름은 숫자로 시작할 수 없다. 그러나 숫자를 이름에 포함하는 것은 가능하다. 이름에는 공백이 들어갈 수 없다. 자바의 예약어를 변수 이름으로 사용할 수 없다. 변수 이름에는 영문자(a-z , A-Z), 숫자(0-9), 달러 기호($) 또는 밑줄(_)만 사용할 수 있다.
관례
소문자로 시작하는 낙타 표기법. 변수이름은 소문자로 시작하는 것이 일반적이다. 여러 단어로 이루어진 변수 이름의 경우, 첫 번째 단어는 소문자로 시작하고 그 이후의 각 단어는 대문자로 시작하는 낙타 표기법을 사용한다.
변수 명명 규칙 정리
클래스는 대문자로 시작, 나머지는 소문자로 시작
자바에서 클래스는 이름의 첫 글자는 대문자로 시작한다. 그리고 나머지는 모두 첫 글자를 소문자로 시작한다. 여기에 예외가 딱 2가지 있는데, 상수는 모두 대문자를 사용하고 언더바로 구분한다. 패키지는 모두 소문자를 사용한다.
Touch background to close