딸기말차

[Java] 8. Generic, SOLID, Lambda, Singleton, Database 본문

Bootcamp/Java

[Java] 8. Generic, SOLID, Lambda, Singleton, Database

딸기말차 2023. 7. 3. 22:23

엔코아 플레이데이터(Encore Playdata) Backend 2기 백엔드 개발 부트캠프 (playdata.io)

 

백엔드 개발 부트캠프

백엔드 기초부터 배포까지! 매력있는 백엔드 개발자 포트폴리오를 완성하여 취업하세요.

playdata.io


1. 제네릭 (Generic)

제네릭이란, 클래스에 다양한 데이터 타입을 사용하기 위한 선언이다. 데이터 타입을 작성하는 곳을 <T> 로 표기함으로서 사용할 수 있고, 이렇게 제네릭을 적용할 시 데이터 타입이 컴파일 할 때 적용되기 때문에 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다는 장점이 존재한다. 또한, 비슷한 기능이 많을 경우 코드의 재사용성이 높아진다는 장점도 존재한다.

하지만 단점도 존재하는데, 처음 코드를 마주하는 경우 이 클래스나 메서드가 필요하는 타입이 무엇인지 알기가 힘들다는 것이다. 이는 여러 상속이 섞일 수록 더 어려워지기 때문에, 제네릭을 사용할 땐 자신이 현재 어떤 객체를 사용할지, 객체가 필요로하는 타입은 무엇인지 확실히 알 수 있을 때 사용하는 것이 좋다.

 // 1. 일반 클래스
 public class Test {
  	private int a = 10;
  	
  	public Test() {
  	}
  	
  	public int getTest() {
  		return this.a;
  	}
  
  	public void setTest(int a) {
  		this.a = a;
  	}
 }
 // 2. 제네릭 적용 클래스
public class Test<T> {
	private <T> a;

  	public Test() {
 	}
  
  	public <T> getTest() {
 		return this.a;
  	}
  
  	public <T> setTest(<T> a) {
  		this.a = a;
  	}
}
// 3. 제네릭 적용 클래스 사용
public class Main {
  	public static void main(String[] args) {
  		Test<String> t1 = new Test<String>();
  		t1.setTest("addada");
  
  		Test<Integer> t2 = new Test<Integer>();
  		t2.setTest(3131); 
  	}
}

2.  SOLID

SOLID란 객체지향의 5가지 원칙으로, 유지보수와 확장이 쉬운 SW를 만드는데 적용하는 대표적인 원칙이다.

 

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

sw의 설계 부품 (클래스, 메서드 등) 은 단 하나의 책임만을 가져야한다. 즉, 독립적이어야한다.

/*
 * SRP가 지켜지지 않은 경우
 */
public class Person {
	public String job = "";
	
	/*
	 * 기본 생성자는 생략 가능하지만, 반드시 명시적으로 코딩
	 * 기본생성자가 없을 경우, 컴파일 시 기본생성자는 자동으로 만들어진다.
	 * 다만, Java Web 또는 Spring Framework에서는, xml문서를 통해 객체 생성이 이루어지기 때문에 반드시 명시한다.
	 */
	public Person() {
	}
	
	public Person(String job) {
		this.job = job;
	}
	
	/*
	 * 현재 job을 저장하는 책임에, 그 사람의 job을 구별해야하는 책임까지 추가 되었다.
	 * 때문에 추상 메서드로 분리 사용해야 하는 부분인데, 일반클래스라 사용할 수 없다.
	 */
	public void work() {
		if (this.job.equals("Programmer"))
			System.out.println("코딩하는 사람");
		else if (this.job.equals("PM"))
			System.out.println("기획하는 사람");
	}
}

위 코드를 보면 Person 이라는 클래스는 사람의 직업을 저장하는 클래스인데, work() 의 존재 때문에 사람의 직업을 구별까지 해야하는 책임이 추가되었다. 때문에 SRP를 위반했다고 볼 수 있다.

/*
 * SRP가 지켜진 경우
 */
public class SRP { // 대표클래스 SRP
	public SRP() {
	}
	
	
	// 클래스 내부에 클래스 선언 및 접근제어자 사용이 가능하다.
	public class InnerClass {
	}
	public void method() {
		// 메서드 내부에 클래스 선언도 가능하다.
		class InnerMethod {		
		}
	}
}

// 서브 클래스는 대표클래스의 접근제어자를 따라가기 때문에 접근제어자를 사용할 수 없다.
abstract class Person2 {
	
	// 서브클래스엔 접근제어자를 사용할 수 없지만 메서드에는 사용할 수 있다.
	public abstract void work();
}

class Programmer extends Person2 {
	@Override
	public void work() {
		System.out.println("코딩하는 사람");
	}
}

class PM extends Person2 {
	@Override
	public void work() {
		System.out.println("기획하는 사람");
	}
}

서브클래스를 사용하여 조금 복잡해 보일 수 있지만, 요지는 SRP 클래스와 Programmer 클래스, PM 클래스가 서로 분리 되어 각각의 역할을 수행하고 있다는 점이다. 때문에 SRP를 지켰다고 볼 수 있다.

 

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

sw를 설계할 때 기존의 코드를 변경하지 않고 기능을 수정하거나 추가할 수 있도록 설계해야한다.

/*
 * OCP가 지켜지지 않은 경우
 */
public class CarKey { // 대표 클래스
	CarA myCar;
	
	public CarKey() {
	}
	
	public CarKey(CarA car) {
		this.myCar = car;
	}
	
	public void open() {
		System.out.println("문열림");
	}
	
	public void turnOn() {
		System.out.println("시동걸림");
	}
	
	public void turnOff() {
		System.out.println("시동 끔");
	}
	
	public void lock() {
		System.out.println("문닫힘");
	}
}

class CarA { // 서브 클래스
	public CarA() {
	}
}

위 코드를 보고, 만약 동일한 기능을 가진 SmartPhoneKey라는 클래스를 추가로 만든다 생각해보자. 그럼 내부기능 메서드를 또 만들어줘야하고, 이는 OCP를 지켰다고 할 수 없을 것이다.

/*
 * OCP가 지켜진 경우
 */
public interface CarKey2 {
	public void open();
	public void lock();
	public void turnOn();
	public void turnOff();
}

class Key implements CarKey2 {	
	public Key() { // 기본생성자
	}

	@Override
	public void open() {
		System.out.println("Key로 열기");
	}

	@Override
	public void lock() {	
	}

	@Override
	public void turnOn() {	
	}

	@Override
	public void turnOff() {
	}	
}

class SmartKey implements CarKey2 {	
	public SmartKey() { // 기본생성자
	}
	
	@Override
	public void open() {
		System.out.println("SmartKey로 열기");
	}

	@Override
	public void lock() {	
	}

	@Override
	public void turnOn() {	
	}

	@Override
	public void turnOff() {
	}	
}

이처럼 interface를 사용하여 공통 메서드를 구현하고, implements를 하여 사용하면 기존의 코드 변경 없이 기능만 변경하는 등 작업을 수행할 수 있을 것이다. 때문에 OCP를 지켰다고 할 수 있다.

 

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

자식 클래스는 부모 클래스에게 가능한 행위를 수정할 수 있어야 한다.

public static void main(String[] args) {
    // extends Person -> 자기타입이 아닌 부모타입으로 치환 -> 부모타입으로 Up Casting
    Person p1 = new Programmer(); 
    Person p2 = new PM();

    // 부모 클래스 타입으로 저장 받아도 자식클래스는 work() 메서드 사용가능
    p1.work(); 
    p2.work();

    Object p3 = new PM();
//	p3.work(); // 사용불가 -> Object로 선언 시 부모클래스의 메서드는 사용할 수 없다.

    // 1. 부모클래스 타입으로 형변환
    Person p4 = (Person)p3; // Object에서 Person으로 치환 -> Down Casting
    p4.work();
    // 2. 본인클래스 타입으로 형변환
    PM p5 = (PM)p3; // Object에서 PM으로 치환 -> Down Casting
    p5.work();

    /* 설계 시 주의사항
     * 자식클래스의 멤버(필드) 변수는 선언하지 않는다.
     * 부모클래스는 자식클래스의 멤버(필드) 변수를 알 수 없기 때문이다.
     */
}

위 코드는 Person이라는 부모 클래스와, Programmer, PM이라는 자식 클래스가 존재한다.

부모 클래스는 자식 클래스 내에만 존재하는 데이터를 모르지만, 자식 클래스는 부모 클래스에 존재하는 데이터를 안다.

때문에 자식 입장에서는 객체를 선언할 때 자신의 타입이 아닌 부모 타입으로 치환하여 만들수도, Object 타입을 부모 타입으로 치환하여 선언하거나 부모 타입을 자신의 타입으로 치환해 만들수도 있다.

 

4. 인터페이스 분리 원칙 (ISP, Interface Segregation Principle)

하나의 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야한다. 또한 하나의 일반적인 인터페이스 보다는 여러개의 구체적인 인터페이스가 낫다.

/*
 * 상위 클래스에서 공통부분을 구현하고, 하위 클래스가 따로 구현할 수 밖에 없는 메서드만 구현하도록 설계 
 */
public abstract class Person {
	/*
	 * 기본생성자를 보유하고 있지만 new()를 이용해 단독 객체 생성은 불가능하다.
	 * 클래스 자체가 추상클래스이기 때문이다. 추상클래스의 생성자는 상속 부분에서만 사용가능하다.
	 */
	public Person() { 
	}
	
	public abstract void work(); // 추상메서드 : 메서드명 까지만 선언한다.
	
	public void eating() {
		System.out.println("먹다");
	}
	
	public void sleeping() {
		System.out.println("자다");
	}
}

위 추상클래스를 선언하고, 다음 두 가지 클래스를 보자.

/*
 * ISP 미적용
 */
public class Programmer extends Person{
	public Programmer() {
	}

	@Override
	public void work() {	
	}
	
	public void Eating() { // Person에 있는 Eating 메서드를 재정의
	}
	
	public void Sleeping() { // Person에 있는 Sleeping 메서드를 재정의
	}
}

위 코드를 보면 추상클래스에 선언해놓은 추상메서드를 사용한 것이 아니라, 일반메서드를 재정의하여 사용하고 있다. 이렇게 쓸바에는 추상클래스의 메서드를 추상메서드로 전환하거나, 인터페이스를 사용하는 것이 좋다.

/*
 * ISP 적용 : 공통 모듈 개발 시 적용
 */
public class Teacher extends Person{
	public Teacher() {
	}

	@Override
	public void work() {
	}
	
	// 이렇게 Teacher 클래스만 사용할 메서드인 경우에만 만드는 것이 좋다.
	public void teaching() {
		System.out.println("신입 사원 교육");
	}
}

위 코드를보면, 추상클래스에서 선언한 메서드를 상속받고 자신이 사용할 메서드만 새롭게 선언하고 있다.

이런식으로 부모클래스는 각 자식클래스의 공통부분을 구현받도록 설계하고, 자식클래스는 자신만의 메서드를 구현할 수 있도록 설계하는 것을 AOP (관점 지향 프로그래밍) 이라고 한다. 때문에 이 경우 ISP를 지켰다고 볼 수 있다.

 

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

의존관계를 맺을 때, 변화하기 쉬운 것보다 변화하기 어려운 것에 의존해야한다.

DIP를 이야기 하기 위해선 의존성이라는 것의 개념을 빼놓을 수 없다. 의존성이란, 결합도가 낮고 응집도가 높아야 한다는 것을 의미한다.

즉, 두 객체를 생성한다는 상황이 있으면 두 객체가 서로 독립적으로 각각 생성될 수 있어야 한다는 것이다.

/*
 * DIP를 지키지 않은 경우
 */
public class Test {
  	public Test() {
  	}
  	public Test(Test2 test2) {
  	}
  }
  
public class Test2 {
  	public Test2() {
  	}
  	public Test2(Test test) {
  	}
  }
  
  Test t = new Test(new Test2(new Test()));
  Test2 t = new Test2(new Test(new Test2()));

위 예시는 클래스로서 의미가 없다. Test를 만드려면 Test2를 인자로 받아야하고, 이 Test2는 또 Test를 인자로 받고 있기 때문이다. 이처럼 클래스 간 결합도가 너무 높으면 좋은 코드가 될 수 없다.

/*
 * DIP를 지키지 않은 경우2
 */
public void avg() {
  	sum();
  	count();
}
  
public void sum() {
}
  
public void count() {
}

위 코드를 보면 avg() 내부에 sum()과 count()가 존재하고, 때문에 avg()는 sum()과 count()에 의존성을 갖게 되어 결합도가 강해져 버린다. 이를 해결하기 위해 보통 return 값을 설정해, 자신의 로직만 해결하여 응집도를 상승시킨다.

/*
 * DIP를 지킨 경우
 */ 
public void sum() {
}
  
public void count() {
}

public double avg(int sum, int count) {
	return avg;
}

3.  Lambda (람다)

람다표현식이란 익명 메서드를 지칭하는 용어로 -> 키워드를 사용하고, java 1.9부터 지원하기 시작했다.

장점 : 코드가 간결해지고, 불필요한 연산을 줄일 수 있고, 병렬 처리가 가능하다.

단점 : 람다 stream 사용에 단순 for나 while을 사용 시 성능이 오히려 떨어지고, 너무 많이 사용 시 가독성이 떨어진다.

 

우선 람다표현식을 사용하지 않은 경우를 보자.

// 익명(이름없는) 객체를 이용한 자바 코드
new Thread(new Runnable() {

    @Override
    public void run() {
        System.out.println("!!!!");
    }
}).start();

위 코드에서 익명 객체란, 사용 후 일정 시간이 지나면 JVM의 가비지 컬렉터가 알아서 소멸 시키는 객체로, 메모리를 효율적으로 사용할 수 있는 장점이 있다. 

이 코드에 람다표현식을 적용하면 어떻게 될까?

// lambda
new Thread(() -> {
    System.out.println("!!!!");
}).start();

인터페이스에 선언한 추상메서드를 implements해 구현하는 것도 가능하다.

interface Func {
	public int calc(int n1, int n2);
}

Func add = (int a, int b) -> a + b;

4. SingleTon

SingleTon이란 java의 디자인 패턴 중 대표적인 패턴으로, 하나의 어플리케이션에 단 하나의 객체만 존재하도록 설계하는 패턴을 의미한다.

* 1. 객체 저장 변수는 static을 이용하여 공유 객체로 선언
* 2. 생성자는 반드시 private로 선언해 외부에서 객체를 생성하지 못하도록 한다
* 3. public 메서드를 이용하여 생성 된 객체를 반환 받는다.
* 예) Workbook.createWritable() 처럼 주로 DB에 접근할 때 사용한다.
public class SingletonClass {

	public static SingletonClass single = null;
	
	private SingletonClass() {
	}
	
	public static SingletonClass getInstance1() {
		single = new SingletonClass();
		return single;
	}
	
	public static void getInstance2() {
		single = new SingletonClass();
	}
	
}

이렇게 SingleTon을 유지하는 클래스를 만든 후, 사용하는 두 가지 방법을 main에 구현하였다.

// 1. 생성 된 객체를 return 받는 형태
SingletonClass s = SingletonClass.getInstance1();

// 2. 생성 된 객체를 return 받지 않는 형식
SingletonClass.getInstance2();

5. Database

DB에는 여러 DB가 존재하는데, 그 중 실습 중 사용하는 관계형 DB인 MySQL에 관한 설명이다.

DB : 관계형 DB
ex) Oracle / MySQL / SQLite
빅 데이터 : NoSQL을 이용한 DB
ex) MongoDB / MariaDB(MySQL + MongoDB)

1) MySQL : 관계형 데이터베이스, 관계형 데이터베이스란 테이블과 테이블 간의 관계를 맺어 구축하는 것이다.

2) SQL과 NoSQL의 차이
SQL : 구조적인 질의 언어 (DB가 알 수 있는 언어)로, 반드시 데이터의 타입이 존재한다. (DB 개발사가 만들어 놓은 타입만 사용)
NoSQL : 기존 구조적인 언어가 아니기 때문에, 데이터의 타입이 존재하지 않는다. 마치 Java Collection의 Map과 같은 형태이다.

3) 관계형 데이터베이스의 구조
하나의 데이터베이스가 존재하고, 데이터베이스 내에는 테이블이 존재한다.
테이블 내에는 데이터를 저장할 수 있는 필드들이 존재한다.
Oracle : 데이터베이스 개념 -> 사용자 계정명
MySQL : 데이터베이스로 존재

4) 데이터베이스의 필드 타입 : 크기는 byte 단위이다.
정수 : int(크기)
실수 : float(크기)
문자열 : char(크기) / varchar(크기) / text -> 약 23000자 정도를 저장
이미지 저장 : blob

5) 문자열 데이터 종류 및 주의점
 데이터 별 크기
 1. 1byte : 영어, 특수문자, '숫자'
 2. 2byte : 한글

ex) char(4) : 최대 abcd 4글자 저장, 한글은 ㄱㅇ / 강가 같이 2글자 저장
     varchar(4) : abcd / ㄱㅇ / 강가

char(4) 에 ab 두글자만 넣었을 경우, 2byte 데이터지만 여전히 공간은 4byte를 가지고있다.
반면 varchar(4) 에 ab 두글자만 넣었을 경우, 데이터 사이즈에 맞게 2byte로 줄어든다.

MySQL을 사용하기 위해서는 DB를 생성 후 테이블을 생성하고, 원하는 동작에 맞는 Query를 사용해야 한다.

DB 생성 및 사용, TABLE 생성
1) MySQL에서 데이터베이스 생성
CREATE DATABASE DB명;

2) MySQL에서 데이터베이스 사용
USE DB명;
MySQL에서 테이블 생성 : 생성 전 반드시 USE DB명; 이 먼저 선행되어야한다.

3) CREATE TABLE 테이블명 (필드명 데이터타입(크기)); -> 필드명과 SQL은 대소문자를 구별하지 않는다.

ex)
create database test; // DB 생성
use test; // DB 사용 시작
create table member(id int(4), name varchar(10)); // DB에 id, name이라는 필드를 가진 member 라는 테이블을 생성

ANSI Query (표준 쿼리, CRUD) : 데이터 삽입, 조회 수정, 삭제
C (생성 된 테이블에 데이터 삽입) : insert into (필드명) values (데이터);
R (생성 된 테이블의 데이터 조회): select 필드명 from 테이블명;
U (생성 된 테이블의 데이터 수정): update 테이블명 set 필드명 = 신규데이터 where 검색필드 = 검색값;
D (생성 된 테이블의 데이터 삭제): delete from 테이블명 where 검색필드 = 검색값;

이때 주의할 점이 있다면, MySQL은 수정, 삭제, 삽입이 바로 적용되어 되돌릴 수 없다. (Auto commit)
반면 Oracle은 바로 적용이 되지 않는다. 때문에 수정, 삭제, 삽입 작업 이 후 commit; 이라는 명령어를 내려야 한다.

이렇게 DB를 생성한 후, Java에 연결해 사용하려면 다음과 같은 순서를 따라야 한다.

* 1. mysql 접속 드라이버 로드
* Driver.class -> com.mysql.cj.jdbc.Driver : 접속 드라이버명
* 
* 2. mysql 서버 접속
* 서버 접속 프로토콜 : jdbc:mysql://
* 서버 주소 : localhost
* 데이터 통신 게이트 (port 번호) : 3306
* 사용할 DB 명 : test
* -> jdbc:mysql://localhost:3306/test
* 서버 관리자 id : root
* 서버 관리자 pw : 설치 시 입력한 pw
* 접속 결과를 저장하는 변수 : Connection 변수명 
* 
* 3. mysql에 요청 : SQL -> SQLException
* 	1) Statement : 쿼리문 내에 변수가 포함되지 않을 경우
* 	2) PreparedStatement : 쿼리문 내에 변수가 포함될 경우
* 
* 4. 결과처리 -> SQLException
* select * from 테이블명의 결과를 ResultSet 객체로 전달 받아 사용한다.
* ResultSet -> while 문을 통해 레코드 하나 (한줄) 씩 읽을 수 있다.
* while(ResultSet.next() {
* }
* 
* 5. 자원해제
*  1) ResultSet.close();
*  2) Statement.close(); / preparedStatement.close();
*  3) Connection.close();

해당 내용을 2번, mysql 서버 접속까지 실습하였다.

// 1. mysql 접속 드라이버 로드
try {
    Class.forName("com.mysql.cj.jdbc.Driver");
    System.out.println("드라이버 로드 성공");
} catch (ClassNotFoundException e) {
    System.out.println("ClassNotFoundException: " + e.getMessage());
}

// 2. mysql 서버 접속
String url = "jdbc:mysql://localhost:3306/";
String id = "root";
String pw = "0000";
String dbName = "test";
url += dbName;

Connection con = null;
try {
    con = DriverManager.getConnection(url, id, pw);

} catch (SQLException e) {
    System.out.println("SQLException: " + e.getMessage());
}

6. 8일차 후기

금일 수업내용 중 반드시 기억하고 있어야하는 것은 SOLID와 Singleton 패턴이라고 생각한다. 우리는 Java Backend 수업을 듣고 있고, 스프링 및 스프링부트를 개발하는데 있어 위의 두가지는 반드시 기억하고 그 형태에 맞게 구현하려고 노력해야하기 때문이다.

 

또한 개인적으로 관심을 가지고있던 Lambda에 관한 수업도 진행하였는데, 람다표현식은 언뜻보기엔 무슨용도인지 이해하기 어렵지만 추후에 stream과 연계되어 자주 사용하는 문법이다. 이에대해 추가로 학습해, 능숙히 사용할 수 있도록 연습해두면 많은 도움이 될 것이라 생각한다.