데이터베이스에 비밀번호 저장 시, 개인정보 보호법에 따라 반드시 SHA2-256 해쉬 암호화된 결과로 저장해야 합니다.

암호화 알고리즘을 사용하기 위해 Spring-Security Sub-Project가 제공하는 의존성을 추가해야 합니다.

 

[아파치 메이븐/의존성] - 라이브러리

 

 


 

 

비밀번호의 암호화 저장도 중요한만큼, 입력된 평문암호와 DB에 저장되어 있는 해쉬 알고리즘의 비교를 통해 암호 일치여부를 검증하는 방법도 알아야 합니다.

 

해쉬 알고리즘은 원본 데이터를 불가역적인 변환을 통해 암호화된 결과(Hash)를 얻습니다.

즉, 해쉬 알고리즘은 복원이 불가능한 단뱡향 암호화 알고리즘 중 하나라고 할 수 있습니다.

 

가장 많이  사용하는 비밀번호 암호화 알고리즘은 'BCrypt'라는 알고리즘이며 스프링 시큐리티는 BCryptPasswordEncoder 클래스를 제공합니다.

 

 

평문 암호(Plain Text)와 해쉬 암호(Cipher Text)를 콘솔에 로그로 출력하였습니다.
@Log4j2
@NoArgsConstructor

@TestInstance(Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class PasswordEncoderTests {
	
//	@Disabled
    @Tag("fast")
    @Test
    @Order(1)
    @DisplayName("testBCryptPasswordEncoder")
    @Timeout(1L)
    void testBCryptPasswordEncoder() {
        log.trace("testBCryptPasswordEncoder() invoked.");
		

        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        log.info("\t encoder({}) invoked.", encoder);
		
        String plainText = "0123456789";
        String cipherText = encoder.encode(plainText);
        
        log.info("\t cipherContext: {}", cipherText);
		
		
	} // testPasswordEncoder
	
} // end class

암호화된 평문 암호(해쉬 암호)

 

 


 

 

평문 암호와 해쉬 암호를 assertj를 활용하여 비교합니다.
@Log4j2
@NoArgsConstructor

@TestInstance(Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class PasswordEncoderTests {
	
    @Tag("fast")
    @Test
    @Order(1)
    @DisplayName("testBCryptPasswordEncoder")
    @Timeout(1L)
    void testBCryptPasswordMatches() {
        log.trace("testBCryptPasswordMatches() invoked.");

		
        String plainText = "0123456789";
		
        PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        // BCryptPasswordEncoder encoder2 = new BCryptPasswordEncoder();
        String cipherText = encoder.encode(plainText);
		
        boolean isMatched = encoder.matches(plainText, cipherText);
		
        assertThat(isMatched).isEqualTo(true);
        log.info("\t isMatched: {}", isMatched);
		
    } // testBCryptPasswordMatches
	
} // end class

평문 암호와 해쉬 암호를 비교 검증

 

 

BCrypt는 동일한 평문 암호를 입력하더라도 매번 다른 해시 암호를 도출합니다.

 

 

 

'Java > Design Patterns' 카테고리의 다른 글

프록시 패턴  (0) 2023.08.13
어댑터 패턴  (0) 2023.08.13

 

 

 

'Record'는 변경할 수 없는(immutable) 데이터 개체를 쉽게 만들 수 있도록 하는 자바의 새로운 유형의 클래스입니다.

Java14 이후 버전부터 지원합니다.

 

 


 

 

Record

 

데이터 모델 POJO(Plain Old Java Object)에서 반복적인 상용구 코드를 줄이기 위해 'Record'가 도입되었습니다.

즉, 'Record'는 반복적인 개발을 단순화하고 효율성을 개선해서 오류의 위험을 최소화합니다.

 

Record는 파라미터에 접근제한자, final, 생성자 사용이 불가능합니다.

 

 

다음은 Record의 Syntax입니다.

 

record className() {

} // 클래스명

 

 

이해를 돕기 위해 간단한 코드를 작성하였습니다.

 

@Log4j2
public class Record_2 {

	
    public static void main(String[] args) {
        Person person = new Person("Leees", 23);
        log.info("name: {}, age: {}", person.name(), person.age());
        log.info("toString: {}", person.toString());

    } // main

} // end class


// @AllArgsContructor
// @NoArgsConstructor
// 파라미터에 final, 접근제한자 사용불가
record final Person(String name, int age) {;;}

 

위의 코드에서도 알 수 있듯이, 'Record'는 필드 선언과 생성자 선언을 하나로 결합한 것으로 볼 수 있습니다.

 

 

Record

 

그런데 로그를 출력할 때, 기존의 Getter가 아닌 단순 필드명으로 필드의 값을 받아오는 것을 확인할 수 있습니다.

이러한 방식을 Fluent API라고 합니다.

 

 


 

 

Fluent

 

일반적으로 필드의 값을 받아올 때, get + 필드()를 사용합니다.

그런데 Getter 메서드는 두 가지의 종류가 있습니다.

 

  1. get + 필드명()
  2. 필드명()

2번처럼 선언하는 Getter 방식을 Fluent API라고 합니다.

Lombok의 @Values 어노테이션은 다양한 기능을 제공하는데 대표적으로 Getter가 있습니다.

 

여기서 @Values가 제공하는 Getter는 일반적인 get + 필드명()으로 제공되는데, Fluent API 방식으로 변경할 수 있습니다.

 

@Log4j2
public class Record_2 {

	
    public static void main(String[] args) {
        Humun humun = new Humun("홍길동", 24);
		
    } // main

} // end class


// @Accessors 어노테이션에서 fluent는 default가 false입니다.
// 여기서 fluent를 true로 변경하면 @Value 어노테이션의 Getter를 
// Fluent API 방식으로 사용할 수 있습니다.
@Accessors(fluent = true)
@Value
class Humun {
    String name;
    int age;
    
} // end class

 

 

Lombok의 @Accessors 어노테이션을 사용하면 Fluent 방식으로 변경된다. 

 

 

@Value 어노테이션은 필드 보호를 위해 private final을 강제합니다.

 

 

 

 

 

 

 

 

'Java' 카테고리의 다른 글

객체 입출력 스트림  (0) 2023.08.18
보조 스트림  (0) 2023.08.17
파일 클래스  (0) 2023.08.17
Exception Handling  (0) 2023.07.28
Exception  (0) 2023.07.27

 

 

 

직렬화는 Object의 필드들(정보)을 바이트 코드로 변환하는 것을 의미합니다.

역직렬화는 바이트 코드들을 본래의 필드로 복구하는 것을 의미합니다.

 

 


 

 

클래스를 생성할 때 Serializable를 Implements 해야 합니다.

만약 출력하고 싶지 않은 필드가 있다면 해당 필드에 transient 키워드를 붙이면 됩니다.

 

다음은 직렬화에 대한 코드입니다.

 

 

ClassA

 

@Data
public class ClassA implements Serializable {
    private int field1;             // 일반 필드 생성
    ClassB field2 = new ClassB();   // ClassB 객체 생성
    static int field3;              // 정적 필드 생성
    transient int field4;           // 한정자(transient) 필드 생성, 직렬화 제외
	
} // end class

 

 

클래스B와 클래스C는 간단한 필드만 선언되어 있습니다.

 

 

ClassB

 

@ToString
@NoArgsConstructor
public class ClassB implements Serializable {
    int field1;
	
} // end class

 

 

ClassC

 

@ToString
@NoArgsConstructor
public class ClassC implements Serializable {
    int field1;
	
} // end class

 

 

메인 스트림에서 지정한 경로에 필드의 정보를 출력합니다.

 

SeriaizableWriter

public class SerializableWriter {

	
    public static void main(String[] args) throws Exception{
        // @Cleanup 어노테이션으로 자동 자원 해제
        @Cleanup FileOutputStream fos = new FileOutputStream("C:/Temp/Object.dat");
        @Cleanup ObjectOutputStream oos = new ObjectOutputStream(fos);
		
		
        ClassA classA = new ClassA();   // 기본 생성자로 객체 생성
        
        classA.setField1(1);            // 고유속성 초기화
        classA.field2.field1 = 2;       // 부품필드 초기화
        ClassA.field3 = 3;              // 정적필드 초기화
        classA.field4 = 4;              // transient 한정자가 붙은 필드
		
		
        // ClassA타입의 객체를 파일에 출력(ObjectOutputStream)
        oos.writeObject(classA);
        oos.flush();
    } // main
	
} // end class

 

위 실행 클래스가 정상적으로 동작되었다면, Temp 폴더에 Object.dat 파일이 생성됩니다.

 

 

이제 Object.dat 파일을 읽는 실행 클래스를 만들겠습니다.

 

@Log4j2
public class SerializableReader {

    public static void main(String[] args) throws Exception{
        @Cleanup FileInputStream fis = new FileInputStream("C:/Temp/Object.dat");
        @Cleanup ObjectInputStream ois = new ObjectInputStream(fis);
		
		
        ClassA objA = (ClassA) ois.readObject();
		
        log.info("1.field1: {} ", objA.getField1());
        log.info("2.field2.field1: {} ", objA.field2.field1);
        log.info("3.field2: {} ", ClassA.field3);
        log.info("4.field3: {} ", objA.field4);
    } // main

} // end class

 

입력받은 각 필드의 키워드는 다음과 같습니다.

 

  • field1 = classA_private int field1 = 1
  • field2 = classA_classB_int field1 = 2
  • field3 = classA_static int field3 = 3
  • field4 = classA_transient int field4 = 4

 

다음은 출력 결과입니다.

 

조회 결과

 

보시는 것처럼,

Serializable를 Implements하는 클래스에 있는 필드들의 출력 파일을 입력받았는데

field3 과 field4는 결과값이 없는 것을 알 수 있습니다.

그렇다면, 파일을 출력하는 과정에서 field3와 field4가 출력되지 않았다고 예상됩니다.

 


 

가장 먼저 field 4는 transient 키워드가 붙어있기 때문에 애초에 출력이 되지 않습니다.

field3를 출력하지 못하는 이유는 메모리 영역에서 알 수 있습니다.

 

객체의 정보(필드)는 힙 메모리 영역에 저장되는 반면 정적 필드는 static 영역에 저장됩니다.

 

즉, 객체 입출력 스트림은 객체의 정보(필드)를 출력하고 읽는 스트림인데,

일반 필드가 저장되어 있는 힙 영역정적 필드가 저장되는 static 영역은 완전히 별개의 영역이기 때문에 입력받지 못하는 것입니다.

 

 


 

마지막으로 간단한 예제를 통해 조금 더 심도있게 살펴보겠습니다.

 

 

Parent

 

@ToString
@NoArgsConstructor
public class Parent {
    // Getter, Setter를 사용하지 않고 도트로 접근하기 위해 public
    public String field1;
	
} // end class

 

 

Child

 

@Log4j2
@ToString
@NoArgsConstructor
public class Child
    extends Parent
    implements Serializable {

    private static final long serialVersionUID = 1L;
    public String field2;
	
    // 자바언어표준스펙에서 정의된 메서드를 사용함, 직렬화
    private void writeObject(ObjectOutputStream out) 
            throws IOException {
        log.trace("writeObject({}) invoked.", out);
			
        // 이 메서드 안에서, 확장받은 부모 객체의 필드가 직렬화 가능하도록
        // 상속받은 부모객체의 필드를 직접적으로 출력
        out.writeUTF(field1);
        out.defaultWriteObject();
    } // writeObject
	
    // 역직렬화
    private void readObject(ObjectInputStream in)
        throws IOException, ClassNotFoundException {
        log.trace("readObject({}) invoked.", in);
		
        // 이 메서드 안에서, 확장받은 부모 객체의 필드가 역직렬화 가능하도록
        // 상속받은 부모객체의 필드를 직접적으로 입력
        field1 = in.readUTF();
        in.defaultReadObject();
    } // readObject
	
} // end class

 

위 코드에서 serialVersionUID 필드는 만약 클래스가 변경(예를 들어, 필드가 추가되거나 삭제)되면 직렬화된 객체의 형식과 해당 클래스의 형식을 맞추기 위해 사용됩니다.

 

serialVersionUID는 직렬화/역직렬화 과정에서 클래스 버전을 확인하고 만약 serialVersionUID의 값이 동일하다면 필드의 정보 상관없이 역직렬화할 수 있습니다.

 

 

Child 클래스에서 선언된 메서드들은 자바 언어 표준 스펙에서 정의된 메서드를 사용하였습니다.해당 메서드들은 직렬화/역직렬화를 할 때, JVM이 자동으로 콜백(callback)을 합니다. 즉, 이 메서드를 사용함으로써 Serializable하지 못한(직렬화/역직렬화가 불가능한) Parent 클래스의 객체 정보도 직렬화/역직렬화를 가능하게 만듭니다.

 

 

다음은 실행 클래스 입니다.

 

@Log4j2
public class NonSerializableParentExample {

	
    public static void main(String[] args) throws Exception{
		
        @Cleanup FileOutputStream fos = new FileOutputStream("C:/Temp/Object.dat");
        @Cleanup ObjectOutputStream oos = new ObjectOutputStream(fos);
		
        // 필드 초기화
        Child child = new Child();
        child.field1 = "홍길동";
        child.field2 = "홍삼원";
        // 부모가 serializable하지 않아도 직렬화 가능함
        oos.writeObject(child);
		
		
		
        oos.flush();
		
        log.info("write done.\n");
		
        // ===================================================
		
        @Cleanup FileInputStream fis = new FileInputStream("C:/Temp/Object.dat");
        @Cleanup ObjectInputStream ois = new ObjectInputStream(fis);
		
        Child childObj = (Child) ois.readObject();
		
        // Serializable하지 않은 Parent Class의 필드 정보는 역직렬화 불가능함
        log.info("1.field1: {}", childObj.field1);
        log.info("2.field2: {}", childObj.field2);
		
    } // main
	
} // end class

 

입출력을 하나의 클래스에서 구현하였습니다.

 

다음은 코드 결과입니다.

 

Child 클래스에서 메서드를 선언했을 때, 결과

 

보시는 것처럼, JVM은 Parent 클래스가 직렬화/역직렬화가 불가능하다는 것을 알고 Child 클래스의 메서드를 자동으로 콜백하였습니다. 

 

만약 Child 클래스에 해당 메서드가 없다면 어떻게 될까요?

 

Child 클래스에서 메서드 주석처리

 

콘솔 결과에서도 알 수 있듯이, Parent 클래스에 속한 field1 필드가 null값으로 입력된 것을 확인할 수 있습니다.

즉, JVM이 직렬화/역직렬화가 불가능한 클래스를 발견하고 콜백할 메서드를 찾지 못하면 직렬화/역직렬화를 못한 상태로 입/출력이 이루어지게 됩니다.

 

 

 

 

 

 

 

 

 

 

 

 

'Java' 카테고리의 다른 글

레코드  (0) 2023.09.06
보조 스트림  (0) 2023.08.17
파일 클래스  (0) 2023.08.17
Exception Handling  (0) 2023.07.28
Exception  (0) 2023.07.27

 

 

 

보조 스트림은 다른 스트림과 연결돼서 문자 변환, 성능 향상 등 다양한 기능을 제공하며,

다른 보조 스트림과 체인처럼 연결되어 사용될 수 있습니다.

만약 보조 스트림을 사용하게 된다면 안쪽 보조 스트림을 가장 먼저 자원해제 해야합니다.

 

 


 

 

1. 문자 변환 보조 스트림

 

문자 변환 보조 스트림은 바이트를 문자로 변환하는 스트림입니다.

 

public class InputStreamReaderExample {

	
    public static void main(String[] args) throws IOException {
        // 바이트 기반의 기본 입력 스트림 생성
        InputStream is = System.in; // 표준입력을 변수에 저장
		
        // 기본 입력 스트림을 문자 기반의 입력 스트림(Reader)로 변환
        // 즉, 기본 스트림에 보조 스트림을 사용함
        Reader reader = new InputStreamReader(is);
		
        // 보조스트림을 사용했으므로, 문자 기반으로 입력 수행
        int readCharNo;
        char[] cbuf = new char[100]; // 문자 기반 배열 생성
		
        // EOF(스트림의 끝)를 만날때까지, 문자기반의 입력 수행
        while ((readCharNo = reader.read(cbuf)) != -1) { // EOF(-1)
            // cbuf 크기만큼의 문자들을 읽어서, 문자열 조각 생성
            String data = new String(cbuf, 0, readCharNo);
            // 문자열 조각 출력
            System.out.println(data);
        } // while
		
        // 보조 스트림 자원 해제 후, 기본 스트림 자원 해제
        reader.close(); // 보조
        is.close();     // 기본
		
    } // main

} // end class

 

사용자가 입력한 내용을 그대로 출력하는 코드입니다.내용을 입력하고 엔터를 누르면 보조스트림이 바이트 코드로 변환 후 프로세스에 전달합니다.

 


 

여기서 윈도우 운영체제에서는 엔터를 누를 때 발생하는 두 개의 문자가 있습니다.

 

  1. Carriage Retrun (CR) : 현 행의 가장 처음 칼럼으로 이동
  2. Line Feed (LF) : 다음 로우로 이동

 

즉, 엔터키를 누르면 CR이 발생하여 현 행의 가장 처음 칼럼으로 이동 후 LF가 발생해 바로 다음 로우로 이동해서 줄 변환이 이루어집니다.

 

반면, Linux나 Unix, Mac 등 다른 운영체제에서는 LF, CR중 1개의 문자만 발생합니다.

 

 


 

다만, 위 코드에서는 CR/LF에 대한 처리를 따르 하고있지 않기 때문에, 사용자가 텍스트 입력 후 엔터를 눌렀다면,

CR/LF도 출력 결과에 포함되어 나오게 됩니다.

 

그렇기 때문에, 입력한 각 줄이 새로운 줄에 출력됩니다.

 

 

 

 

2. 성능 향상 보조 스트림

 

기본 스트림의 입출력 성능을 향상시키는 보조 스트림입니다.

 

네트워크의 버퍼는 데이터의 조각인 패킷을 임시로 저장하는 공간입니다.

이와 유사하게 성능 향상 보조 스트림도 버퍼 기반으로 동작을 수행하는데, 이는 데이터를 한번에 쓰고 읽는데 효율적입니다.

다만, 버퍼의 크기를 너무 크거나 작게 설정하면 오히려 성능 저하를 일으킬 수 있기 때문에 여러 요인을 고려해서 신중하게 버퍼의 크기를 선택해야 합니다.

 

@log4j2
public class FileOutputStreamExample {

	
    public static void main(String[] args) throws Exception{
        long start = 0;
    	long end = 0;
        String sourcePath = "C:/Temp/WINDOWS.X64_193000_db_home.zip";
        String targetPath = "C:/Temp/WINDOWS.X64_193000_db_home_target.zip";
		
        @Cleanup
        FileInputStream fis = new FileInputStream(sourcePath);
        BufferedInputStream bis = new BufferedInputStream(fis);
        @Cleanup
        FileOutputStream fos = new FileOutputStream(targetPath);
        BufferedOutputStream bos = new BufferedOutputStream(fos);
		
		
        // 바이트 배열(100바이트)을 생성 후
        // 소스 파일에 대한 입력스트림에서 바이트들을 읽어서,
        // 타겟 파일에 대한 출력스트림(원래는 출력 버퍼)에 출력하여
        // 복사 기능을 수행
        int readByteNo;	// 한번 바가지로 읽을 때마다, 실제 읽어낸 바이트의
                        // 갯수를 저장할 변수 선언
		
        // 입출력 용도로 사용할 바이트 배열(100바이트) 생성
        byte[] readBytes = new byte[500];
		
        // 입력 버퍼로부터 바가지 크기만큼 읽어내고,
        // 입력 버퍼의 EOF(스트림의 끝)을 만날 대까지 실제로 읽어낸 바이트들을
        // 출력
        
        // ---------------------------------
        start = System.currentTimeMillis();
        // ---------------------------------
        
        while((readByteNo = bis.read(readBytes)) != -1) {   // 복사 기능 수행
            bos.write(readBytes, 0, readByteNo);            // 실제 읽어낸 바이트 개수 사용
        } // while
		
        // ---------------------------------
        end = System.currentTimeMillis();
        // ---------------------------------
        
        // 출력 버퍼 플러싱
        bos.flush();;
		
        log.info("time : {}", end - start);
    } // main
	
} // end class

 

성능 향상 보조 스트림 미사용 시

 

성능 향상 보조 스트림 사용 시

 

성능 향상 보조 스트림의 사용 유무에 따라 효율이 크게 차이나게 됩니다.

 

 

'Java' 카테고리의 다른 글

레코드  (0) 2023.09.06
객체 입출력 스트림  (0) 2023.08.18
파일 클래스  (0) 2023.08.17
Exception Handling  (0) 2023.07.28
Exception  (0) 2023.07.27

 

 

 

File 클래스는 파일 크기, 파일 속성, 파일 이름 등의 정보와 파일 생성 및 삭제의 기능을 제공합니다.

 

 


 

 

먼저 파일 객체를 생성하고 주소를 지정합니다.

저는 현재 Temp 폴더에 file.txt 파일이 없습니다.

 

@Log4j2
public class FileExample {

    // File 객체를 이용해서, CMD의 dir 명령어의 출력결과를 만듬
    public static void main(String[] args) throws IOException{
        // 파일 객체 생성 시, 생성자 매개변수로 File 객체가 표현할
        // 파일이나 디렉터리의 경로를 명시해주면 됨
        // 이때, 지정된 파일이나 디렉토리는 실제 존재하지 않아도 오류가 나지 않음.
        File dir = new File("C:/Temp/Dir");
        File file1 = new File("C:/Temp/file1.txt");
        File file2 = new File("C:/Temp/file2.txt");
        // File file3 = new File("file:///C:/Temp/file3.txt");
		
        log.info("1. dir : {}", dir);
        log.info("1. dir : {}", file1);
        log.info("1. dir : {}", file2);

 

file.txt

 

참조변수가 해당 레퍼런스를 참조하기 때문에, 해당 레퍼런스에 파일이 없어도 잘 출력이 되는 것을 확인할 수 있습니다.

 

 

그렇다면 File 클래스의 메서드를 활용해서 디렉터리를 생성하겠습니다.

 

        // 파일 생성
        // mkdirs는 file 객체가 가지고 있는 전체 경로 상에
        // 존재하지 않는 모든 디렉터리를 만들라는 의미임
        // 즉, 없으면 만들고 있으면 그대로 둠
        if(dir.exists() == false) {
			
        //  dir.mkdir();
            dir.mkdirs();
        } // if
		
        // 없으면 새로운 파일 생성
        if(file1.exists() == false) {
            file1.createNewFile();
        } // if
		
		
        if(file2.exists() == false) {
            file2.createNewFile();
        } // if

 

위 코드에서 exists()는 파일 존재의 유무를 확인하는 메서드입니다.

해당 경로에 파일이 없으면 만들고 있다면 변경사항은 없습니다.

 

 

위 코드들을 활용해서 C드라이브의 Temp폴더의 디렉터리 목록을 보여주는 코드를 작성할 수 있습니다.

 

        File temp = new File("C:/Temp");
		
        // 날짜 객체로부터 포매팅 문자열 생성
        SimpleDateFormat sdf = 
                new SimpleDateFormat("yyyy/mm/dd   a   HH:mm");
		
        // C:/temp 폴더 안에 있는 모든 파일을 배열로 획득
        File[] contents = temp.listFiles();
		
        // 실제 dir 명령의 출력 결과를 재현하는 코드
        System.out.println("    날짜             시간       형태           크기     이름");
        System.out.println("------------------------------------------------------------");
		
        for(File file : contents) {
			
            // 파일/디렉터리의 최종 수정 시간을 얻고, 이를 포매팅 문자열처럼 출력
            System.out.print(sdf.format(new Date(file.lastModified())));
			
            if(file.isDirectory()) {
                // 만약 디렉터리이면 디렉터리 표시기호를 만들어 넣어주고
                System.out.print("\t<DIR>\t\t\t" + file.getName());
            } else {
                // 만약 파일이면 파일 크기와 이름을 표시
                System.out.println("\t\t\t" + file.length() + "\t" + file.getName());
            } // if-else
			
            System.out.println();
        } // enhanced for
		
    } // main
	
} // end class

 

 

 

dir

 

코드 자체는 크게 어렵지 않습니다.

for문은 temp 폴더 내 디렉터리, 파일을 하나씩 순회하며 콘솔에 출력하는 역할입니다.

위 코드는 명령 프롬포트의 명령어인 dir의 결과와 유사한 것을 알 수 있습니다.

 

 

'Java' 카테고리의 다른 글

객체 입출력 스트림  (0) 2023.08.18
보조 스트림  (0) 2023.08.17
Exception Handling  (0) 2023.07.28
Exception  (0) 2023.07.27
익명구현객체  (0) 2023.07.26

 

 

 

프록시 패턴(Proxy Pattern)은 호출에 대한 흐름제어가 목적입니다.

의존 역전 원칙(Dependency Inversion Principle, DIP)를 활용한 디자인 패턴입니다.

 

 


 

의존 역전 원칙(Dependency Inversion Principle, DIP)

DIP는 상위 객체가 하위 객체에 의존해서는 안되며 추상화에 의존해야함을 의미합니다.

 

즉, DIP는 상위 객체가 하위 객체에 직접 의존하기보다는 인터페이스나 추상 클래스를 통해 객체에 의존하라는 것입니다.

 

인터페이스를 통해 하위 객체에 접근하게 되면상위 객체와 하위 객체 간 관계가 느슨해지고 이로 인해  코드의 유연성을 높일 수 있습니다.

 

 

 

프록시 패턴(Proxy Pattern)

 

네트워크에서는 웹 프록시가 존재합니다.

웹 프록시를 간단하게 설명하면,

클라이언트와 서버 사이에서 중개자의 역할을 하면서 부가적인 기능을 통해 로직을 수행합니다.

 

이와 비슷하게, 프록시 패턴은 중개자의 역할이며

실제로 객체가 필요할 때까지 생성을 지연하고 접근을 제어하는 것에 목적을 두고 있습니다.

 

이해를 돕기 위한 간단한 예시 코드입니다.

 

public class IService {
    abstract String runSomething();
    
} // end interface

 

public class Service implements IService {
	
    @override
    public String runSomething() {
    	return "runSomething()"
        
    } // runSomething
    
} // end class

 

Service 클래스는 IService 인터페이스를 구현하고 있고 있습니다.

이제 Service 클래스에 접근을 제어하기 위한 Proxy 클래스를 만들겠습니다.

 

@log4j2
public class Proxy implements IService {
    IService service1;
    
    
    @override
    public String runSomething() {
    	log.trace("Proxy runSomething() Method invoked.");
        
        service1 = new Service();  // IService를 참조하는 Service 객체 생성
        return service1.runSomething();
        
    } // runSomething
    
} // end class

 

위 코드는 IService 인터페이스를 구현하는 Proxy 클래스입니다.

 

@log4j2
public class ClientWithProxy {
    
    
    public static class main(String[] args) {
        log.trace("main({}) invoked.", Arrays.toString(args));
        
        
        IService proxy = new Proxy(); // IService를 참조하는 Proxy 객체 생성
        log.info(proxy.runSomething());
    
    } // main
    
} // end class

 

 

Proxy 클래스를 보면

runSomething() 메서드를 실행시켜야 Service 객체를 생성하고 이용할 수 있도록 하였습니다.

 

만약 여기서 실행 클래스가 이용할 Service 클래스가 늘어난다 하더라도 Proxy 클래스를 이용해 객체를 관리하고 접근할 수 있습니다.

 

 

 

 

'Java > Design Patterns' 카테고리의 다른 글

암호화 알고리즘  (0) 2023.09.14
어댑터 패턴  (0) 2023.08.13

 

 

 

어댑터 패턴은 개방 폐쇄 원칙(Open/Closed Principle, OCP)을 활용한 패턴입니다.

OCP에 대해 먼저 이해하고 어댑터 패턴에 대해 다루겠습니다.

 


 

개방 폐쇄 원칙(Open/Closed Principle, OCP)

 

OCP는 자바 개발자라면 의식하지 않아도 항상 접할 수 있는 환경에 놓여져 있습니다.

 

자바 개발자는 코드를 작성할 때,

이 코드가 Window, MacOS, Linux 등 어느 운영체제에서 쓰일지 하나하나 고려하며 작성하는 개발자는 없습니다.

 

이러한 걱정을 덜어주는 이유는 JVM이라는 존재 덕분입니다.

 

JVM으로 인해 개발자는 내가 작성하는 소스코드가 어느 운영체제에 쓰일지 신경쓰지 않아도 되기 때문에 운영체제의 변화에 닫혀있다고 할 수 있습니다.

반면, JVM은 새로운 운영체제나 기존의 운영체제의 변화에 따라 업데이트됩니다. 이러한 특징은 JVM이 운영체제의 확장에 열려있다고 할 수 있습니다.

 

즉, OCP는 클래스같은 소프트웨어 엔티티는 확장(extends)에 열려있어야 하지만 변화에는 닫혀있어야 한다는 원칙입니다.

 


 

어댑터 패턴(Adepter Pattern)

 

마트에 방문한 손님은 물품을 구매합니다.

마트의 포스기는 제품들의 재고상황, 신제품, 없어진 제품 등을 관리합니다.

손님은 포스기 덕분에 마트의 제품들에 대한 변화에 영향을 받지 않고 편하게 제품을 구매할 수 있습니다.

 

@log4j2
public class Product1{
    
    
    void purchase1{
    	log.info("1번 제품을 구매하였습니다.");
    } // purchase1
    
} // end class

 

@log4j2
public class Product2{
    
    
    void purchase2{
    	log.info("2번 제품을 구매하였습니다.");
    } // purchase2
    
} // end class

 

위 코드는 손님이 구매할 제품군입니다. 제품을 구매하기 위해 포스기가 있어야 합니다.

 

 

@log4j2
public class Pos{
    Purchase1 p1 = new Purchase();
    
    void runService1{
    	p1.purchase1;
    } // purchase1
    
     void runService2{
    	p2.purchase2;
    } // runService2
    
} // end class

 

제품을 구매할 포스기가 구비되었으니 이제 손님은 포스기를 이용해 제품을 구매합니다.

 

 

@log4j2
public class Consumer{
	
    
    public static class main(String[] args){
    	Pos pos = new Pos();
        
        
        pos.runService1();  // 1번 제품을 구매하였습니다.
        pos.runService2();  // 2번 제품을 구매하였습니다.
        
    } // main
    
} // end class

 

 

어댑터 패턴은 간단히 말해,

호출자가 변환기(Converter)를 이용해 피호출자를 호출하는 방식의 디자인 패턴입니다.

 

 

'Java > Design Patterns' 카테고리의 다른 글

암호화 알고리즘  (0) 2023.09.14
프록시 패턴  (0) 2023.08.13

 

 

 

JAVA 8부터 함수적 프로그래밍을 위해 람다식(Lambda Expression)을 지원합니다.

익명구현객체 코딩기법에 비해 코드가 매우 간결해지고,

컬렉션 요소(대용량 데이터)를 필터링하거나 매핑을 해서 쉽게 집계할 수 있습니다.

 

 


 

 

람다식에서는 두 가지 용어가 있습니다.

 

  1. 타겟 타입(Target Type)
    타겟 타입은 람다식으로 구현할 함수적 인터페이스(Functional Interface)를 의미합니다.

  2. 함수적 인터페이스(Functional Interface)
    자바 8이상부터 @FuntionalInterface 어노테이션을 통해 함수적 인터페이스를 정의할 수 있습니다.
    추상메서드가 1개라면, 함수적 인터페이스이지만 어노테이션을 붙이는 이유는
    컴파일러가 컴파일 시에 검사할 수 있도록 하기 위함입니다.

 

 

람다식은 이러한 함수적 인터페이스를 implements 합니다.

implements하는 함수적 인터페이스를 람다식의 타겟 타입(Target Type)이라고 합니다.

 

람다식이 구현하는 익명함수는 익명구현객체와 비슷하지만 약간의 차이가 있습니다.

익명함수는 1개의 추상 메서드만 재정의하지만,

익명구현객체는 추상 메서드의 갯수에 대한 제한이 없습니다.

 

 

여기서 "익명"이란 이름이 없는 것이 아니라 이름을 모르는 것을 의미합니다.

그렇다면, 익명구현객체를 찍어낸 인터페이스는 추상메서드의 개수에 제한이 없습니다.

 

다음은 간단한 람다식 구현 코드입니다.

 

@FunctionalInterface
interface ICalculator {
    public abstract int add(int number1, int number2) throws Exception; // 덧셈
	
} // end Interface

 

@Log4j2
public class App {

	
    // ICalculator 인터페이스를 구현(Implements)
    public static void main(String[] args) throws Exception {
		
        ICalculator calc = (int number1, int number2) -> {
            return number1 + number2;
        };

    } // main

} // end class

 

 

위의 실행 클래스는 생략 전 add() 메서드입니다.

람다식은 여기서 파라미터의 타입을 제거할 수 있습니다.

 

 

@Log4j2
public class App {

	
    // ICalculator 인터페이스를 구현(Implements)
    public static void main(String[] args) throws Exception {
		
        // 생략 전
        //ICalculator calc = (int number1, int number2) -> {
        //    return number1 + number2;
        //};
        
        
        ICalculator calc = (number1, number2) -> {
            return number1 + number2;
        };

    } // main

} // end class

 

 

만약 파라미터가 1개라면 소괄호를 생략할 수 있습니다.

기존에 생성했던 함수적 인터페이스에서 추상 메서드의 파라미터 또한 수정해야 합니다.

 

 

@Log4j2
public class App {

	
    // ICalculator 인터페이스를 구현(Implements)
    public static void main(String[] args) throws Exception {
		
        // 생략 전
        // ICalculator calc = (int number1, int number2) -> {
        //     return number1 + number2;
        // };
        
        // 파라미터 타입 생략
        // ICalculator calc = (number1, number2) -> {
        //    return number1 + number2;
        // };
        
        // 파라미터가 1개일 때, 소괄호 생략 가능
        ICalculator calc = number1 -> {
            return number1 + 100;
        };

    } // main

} // end class

 

 

여기서 중괄호 블록안에 return문만 있을 경우, 중괄호 블록과 return 키워드까지 생략해야 합니다.

 

 

@Log4j2
public class App {

	
    // ICalculator 인터페이스를 구현(Implements)
    public static void main(String[] args) throws Exception {
		
        // 생략 전
        // ICalculator calc = (int number1, int number2) -> {
        //     return number1 + number2;
        // };
        
        // 파라미터 타입 생략
        // ICalculator calc = (number1, number2) -> {
        //     return number1 + number2;
        // };
        
        // 파라미터가 1개일 때, 소괄호 생략
        // ICalculator calc = number1 -> {
        //     return number1 + 100;
        // };
        
        // return키워드, 중괄호 블록 생략
        ICalculator calc = number1 -> number1 + 100;
        
        

    } // main

} // end class

 

 

최종적으로 생략된 코드에서

  • number1 : 파라미터 선언부
  • number + 1 : 메서드 구현부

 

입니다.

 

 

 

 

 

 

+ Recent posts