- SOLID
1) SRP : 단일 책임 원칙
👉 한 클래스는 하나의 책임만 가져야한다.
👉 하나의 책임은 클수도 있고, 작을 수도 있다.
👉 하나의 책임의 기준은 변경이다.
👉 변경이 발생하였을 때, 변경해야 될 부분이 적으면, 단일책임 원칙을 잘 따른 것이다.
👉 클래스를 변경해야하는 이유가 오직 하나여야 한다.
👉 ex) 결제 버튼의 위치가 변경되었지만, 결제 기능에 대한 영향은 없다.
- 코드로 보는 단일 책임 원칙 예시
class Galaxy {
private String serialNumber;
private String cpu;
private String memory;
private int battery;
private double weight;
}
--------------
serialNumber는 고유정보 -> 변화 요소가 아님.
cpu, memory, battery, weight는 특성 정보로 변경이 발생할 수 있음.
특성 정보에 변화가 발생하면, Galaxy 을 변화 시켜야 되는 부담이 발생함.
-------------------------
// 스펙만 관리
class GalaxySpec {
private String cpu;
private String memory;
private int battery;
private double weight;
}
class Galaxy {
private String serialNumber;
private GalaxySpec spec;
public Galaxy(String serialNumber, GalaxySpec spec) {
this.serialNumber = serialNumber;
this.spec = spec;
}
}
2) OCP : 개방/폐쇠 원칙
👉 확장에는 열려있으나, 변경에는 닫혀 있어야한다.
👉 변경을 위한 비용은 줄이고, 확장을 위한 비용은 극대화 해야한다.
👉 객체지향의 장점을 극대화하는 아주 유용한 원리( 다형성 )
👉 템플릿 메서드 패턴 -> 개방/폐쇠 원칙의 아주 좋은 예시
- 코드로 보는 개방/폐쇠 원칙 예시
class Galaxy {
private String serialNumber;
private String cpu;
private String memory;
private int battery;
private double weight;
}
class GalaxySpec {
private String cpu;
private String memory;
private int battery;
private double weight;
}
class Galaxy {
private String serialNumber;
private GalaxySpec spec;
public Galaxy(String serialNumber, GalaxySpec spec) {
this.serialNumber = serialNumber;
this.spec = spec;
}
}
----------
class IPhone {
private String serialNumber;
private String cpu;
private String memory;
private int battery;
private double weight;
}
class IPhoneSpec {
private String cpu;
private String memory;
private int battery;
private double weight;
}
class IPhone {
private String serialNumber;
private IPhoneSpec spec;
public IPhone(String serialNumber, IPhoneSpec spec) {
this.serialNumber = serialNumber;
this.spec = spec;
}
}
class Shaomi {
private String serialNumber;
private String cpu;
private String memory;
private int battery;
private double weight;
}
class ShaomiSpec {
private String cpu;
private String memory;
private int battery;
private double weight;
}
class Shaomi {
private String serialNumber;
private ShaomiSpec spec;
public IPhone(String serialNumber, IPhoneSpec spec) {
this.serialNumber = serialNumber;
this.spec = spec;
}
}
-------------------------------------------------
갤럭시 외에 새로운 핸드폰들이 생겨난다면?
추상화 작업을 통해서, 공통화 하자.
새로운 핸드폰이 추가 되면서, 변경 될 수 있는 부분들을 분리시킴으로써, 코드 수정을 최소화하여,
결합도를 줄이고,응집도를 높혔다.
-------------------------------------------------
class Phone {
private String serialNumber;
private PhoneSpec spec;
public Phone(String serialNumber, PhoneSpec spec) {
this.serialNumber = serialNumber;
this.spec = spec;
}
}
class PhoneSpec {
private String cpu;
private String memory;
private int battery;
private double weight;
}
class Galaxy extends Phone
class IPhone extends Phone
class 샤오미 extends Phone
class Sony extends Phone
3) LSP : 리스코프 치환 원칙
👉 하위타입(자식)은 언제나 상위 타입(부모)로 교체될 수 있어야한다.
👉 정확성을 깨뜨리면 안된다.
👉 원칙을 지키지 않으면, 자식 클래스의 인스턴스를 파라미터로 전달했을 때 메소드가 이상하게 작동할 수 있다.
👉 ex) 어떤 경우라도 정해진 기능을 수행할 것...!
- 코드로 보는 리스코프 치환 원칙 예시
class Rectangle {
private int width;
private int height;
public void setHeight(int height) {
this.height = height;
}
public int getHeight() {
return this.height;
}
public void setWidth(int width) {
this.width = width;
}
public int getWidth() {
return this.width;
}
public int getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
@Override
public void setHeight(int value) {
this.width = value;
this.height = value;
}
@Override
public void setWidth(int value) {
this.width = value;
this.height = value;
}
}
public class Test {
static void testLSP(Rectangle rectangle) {
rectangle.setWidth(5);
rectangle.setHeight(3);
System.out.println(rectangle.getArea());
}
}
public static void main(String[] args){
Rectangle rectangle = new Rectangle();
rectangle.setWidth(5);
rectangle.setHeight(2);
rectangle.getArea(); // 10
Rectangle square = new Square();
square.setWidth(5);
square.setHeight(2);
square.getArea(); // 25
Test.testLSP(new Rectangle());
Test.testLSP(new Square());
}
4) ISP : 인터페이스 분리 원칙
👉 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야한다.
👉 특정 클라이언트를 위하여, 하나의 범용 인터페이스를 제공하는 것 보다 여러개의 인터페이스를 제공하는 것이 더 좋다.
- 코드로 보는 인터페이스 분리 원칙 예시
public interface Phone {
void call(String phoneNumber);
void pay(String cardName);
void wirelessCharge();
}
public class Galaxy implements Phone {
@Override
public void call(String phoneNumber) {
// ..
}
@Override
public void pay(String cardName) {
// ..
}
@Override
public void wirelessCharge() {
// ..
}
}
public class IPhone implements Phone {
@Override
public void call(String phoneNumber) {
// ..
}
@Override
public void pay(String cardName) {
// ..
}
@Override
public void wirelessCharge() {
// ..
}
}
public class SkyPhone implements Phone {
@Override
public void call(String phoneNumber) {
// ..
}
@Override
public void pay(String cardName) {
// ..
}
@Override
public void wirelessCharge() {
// 1. 가만히 냅눈다. - 극강 빌런
// 2. System.out.println("사용하지 않음"); - 그나마 양호
// 3. throw new NotSupporedException(); - 테스트코드에서
}
}
public void main (String args[]) {
Phone phone = new Galaxy();
phone = new Galaxy();
phone = new SkyPhone();
phone.wirelessCharge(); // 1. 에러를 찾는데, 시간이 오래 걸린다. 이건 빌런의 소행이다.
}
5) DIP : 의존관계 역전 원칙
👉 객체들 간의 협력 하는 과정에서 의존 관계가 형성된다.
👉 의존관계 역전 원칙은 이러한 의존 관계를 맺을 때, 어떻게 하면 변화에 용이하게 대응할 수 있을 것인가에 대한 가이드 라인이다.
👉 변하기 쉬운 것과 어려운 것을 구분해야한다.
👉 변하기 쉬운 것 -> 구체적인 행동(ex. 스마트폰으로 전화를 건다, 공중전화로 전화를 건다...)
👉 변하기 어려운 것 -> 흐름이나 개념 같은 추상적인 것 (ex. 전화를 건다, 메세지를 전달한다...)
- 코드로 보는 의존관계 역전 법칙 예시
public interface Phone {
void call(String phoneNumber);
}
public class SmartPhone implements Phone {
@Override
public void call(String phoneNumber) {
System.out.println("스마트폰 : " + phoneNumber);
}
}
public class PublicPhone implements Phone {
@Override
public void call(String phoneNumber) {
System.out.println("공중 전화 : " + phoneNumber);
}
}
public class InternetPhone implements Phone {
@Override
public void call(String phoneNumber) {
System.out.println("인터넷 전화 : " + phoneNumber);
}
}
public class Person {
private Phone phone;
public void setPhone(Phone phone) {
this.phone = phone; // 폰이 계속 바뀜.
}
public void call(String phoneNumber) {
phone.call(phoneNumber);
}
}
public class Main {
public static void main(String[] args) {
Person person = new Person();
person.setPhone(new SmartPhone());
person.setPhone(new InternetPhone());
// 코드 수정 X
person.call("01012341234"); // 스마트폰 전화,
}
}
👉 입력 받은 수의 약수 합 구하기 예제( 매우 쉬움 난이도 )
class Solution {
public int solution(int n) {
// 정수 n을 입력받아 n의 약수를 모두 더한 값을 리턴하는 함수 완성해라
int sum = 0;
for (int i = 1; i <= n; i++) {
if (n % i == 0) {
sum += i;
}
}
return sum;
}
}
👉 설문조사에 따른 성격유형 검사( 문제가 길어서 오래걸렸음... 그리고 매우 비효율적으로 푼듯..)
- 내가 짠 아주 비효율적 코드...
👉 생각의 흐름: 배열방에 성격 유형을 판단할 알파벳을 미리 넣어놓은 배열방과 점수를 받을 배열 방을 미리 만들어 놓고 어차피 index는 같기 때문에 설문조사에 따른 점수를 저장하고, 비교하면서 출력하면 되지 않을까...
import java.lang.reflect.Array;
class Solution {
public String solution(String[] survey, int[] choices) {
// survey = 설문지의 문항 갯수
// choices = 1 ~ 7 가지 선택지 중 고른 번호
char [] arr = {'R','T','C','F','J','M','A','N'};
int [] arr_count = {0,0,0,0,0,0,0,0};
String answer = "";
for(int i =0;i<survey.length;i++){
int f_index = -1;
int b_index = -1;
for(int j=0;j<arr.length;j++){
char front = survey[i].charAt(0);
char back = survey[i].charAt(1);
if (front == arr[j]) {
f_index = j;
}else if(back == arr[j]){
b_index = j;
}
if(f_index >= 0 && b_index >= 0) {
switch (choices[i]) {
case 1:
arr_count[f_index] += 3;
break;
case 2:
arr_count[f_index] += 2;
break;
case 3:
arr_count[f_index] += 1;
break;
case 5:
arr_count[b_index] += 1;
break;
case 6:
arr_count[b_index] += 2;
break;
case 7:
arr_count[b_index] += 3;
break;
default:
break;
}
}
}
}
for(int k = 0;k<arr_count.length;){
if (arr_count[k] > arr_count[k+1]){
answer+=arr[k];
}else if(arr_count[k] < arr_count[k+1]){
answer+=arr[k+1];
}else{
answer+=arr[k];
}
k+=2;
}
return answer;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.solution(new String[]{"AN", "CF", "MJ", "RT", "NA"}, new int[]{5, 3, 2, 7, 5}));
}
}
import java.util.*;
class Solution {
public String solution(String[] survey, int[] choices) {
Map<Character, Integer> map = new HashMap<>();
for(int i = 0; i< survey.length; i++) {
int value = choices[i];
if(value > 0 && value < 4) {
char ch = survey[i].charAt(0);
map.put(ch, map.getOrDefault(ch, 0) + 4 - value);
} else if(value > 4) {
char ch = survey[i].charAt(1);
map.put(ch, map.getOrDefault(ch, 0) + value - 4);
}
}
return new StringBuilder()
.append(map.getOrDefault('R', 0) >= map.getOrDefault('T', 0) ? 'R' : 'T')
.append(map.getOrDefault('C', 0) >= map.getOrDefault('F', 0) ? 'C' : 'F')
.append(map.getOrDefault('J', 0) >= map.getOrDefault('M', 0) ? 'J' : 'M')
.append(map.getOrDefault('A', 0) >= map.getOrDefault('N', 0) ? 'A' : 'N')
.toString();
}
}
👉 Map 자료형을 사용하여 내가 사용했던 count를 위한 정수 배열의 필요성을 없앴다.
👉 Map자료형은 검색에 적합하기 때문에 훨씬 적합.. 편리..효율...!
👉 나는 String자료형도 이어줄 때 + 사용.. 매우 비효율적..
👉 StringBuilder()를 사용하여 String 클레스 객체를 한번만 사용하여 문자열을 이어줌..
1) 문자열 압축예제
👉 일정 반복되는 문자열을 갯수+반복알파벳 으로 묶어 문자열을 압축하자
input = "abcabcabcabcdededededede"
# 모든 경우의 수를 파악하는 방법이라고 보면 되겠다.
# n//2 -> 반절을 넘었을 때 반복될 수 없다.
def string_compression(string):
n = len(string)
compression_length_array = []
for split_size in range(1, n // 2 + 1):
splited = [string[i:i + split_size] for i in range(0, n, split_size)]
compressed = ""
count = 1
for j in range(1, len(splited)):
prev, cur = splited[j - 1], splited[j]
if prev == cur:
count += 1
else:
if count > 1:
compressed += (str(count) + prev)
else:
compressed += prev
count = 1
if count > 1:
compressed += (str(count) + splited[-1])
else:
compressed += prev
compression_length_array.append(len(compressed))
return min(compression_length_array)
print(string_compression(input)) # 14 가 출력되어야 합니다!
2) 괄호 배열 맞추기 예제
from collections import deque
balanced_parentheses_string = "()))((()"
def is_correct_parentheses(string): # 올바른 괄호인지 확인
stack = []
for s in string:
if s == '(':
stack.append(s)
elif stack:
stack.pop()
return len(stack) == 0
def separate_to_u_v(string): # u, v로 분리
queue = deque(string)
left, right = 0, 0
u, v = "", ""
while queue: # 큐사용
char = queue.popleft()
u += char
if char == '(':
left += 1
else:
right += 1
if left == right: # 단, u는 "균형잡힌 괄호 문자열"로 더 이상 분리할 수 없어야 합니다. 즉, 여기서 괄 쌍이 더 생기면 안됩니다.
break
v = ''.join(list(queue))
return u, v
def reverse_parentheses(string): # 뒤집기
reversed_string = ""
for char in string:
if char == '(':
reversed_string += ")"
else:
reversed_string += "("
return reversed_string
def change_to_correct_parentheses(string):
if string == '': # 1번
return ''
u, v = separate_to_u_v(string) # 2번
if is_correct_parentheses(u): # 3번
return u + change_to_correct_parentheses(v)
else: # 4번
return '(' + change_to_correct_parentheses(v) + ')' + reverse_parentheses(u[1:-1])
def get_correct_parentheses(balanced_parentheses_string):
if is_correct_parentheses(balanced_parentheses_string):
return balanced_parentheses_string
else:
return change_to_correct_parentheses(balanced_parentheses_string)
print(get_correct_parentheses(balanced_parentheses_string)) # "()(())()"가 반환 되어야 합니다!
print("정답 = (((()))) / 현재 풀이 값 = ", get_correct_parentheses(")()()()("))
print("정답 = ()()( / 현재 풀이 값 = ", get_correct_parentheses("))()("))
print("정답 = ((((()())))) / 현재 풀이 값 = ", get_correct_parentheses(')()()()(())('))
1) 대기업 코테는 정말 어렵다.. 생각하는 힘 뿐만 아니라 코드의 흐름 자체가 어렵다.. 일단 알맞은 자료구조를 선택하고 활용하여 로직을 구현하는게 정말 어렵다.
2) 자바의 다형성을 위한 법칙들의 존재에 대한 이유를 몰랐는데 코드로 이해하니 알 것 같다.
3) 만약 프로젝트를 진행한다고 했을 때, 공통적인 특성을 묶고, 상속 or 구현을 활용하여 파고든다면 정말 어려울 것 같다.