꿈틀꿈틀 개발일기

20231225 / 주말공부!

by jeongminy

 

자바 공부 하기!

 

 

*객체지향 프로그래밍언어를 사용하는 이유는?
대규모 소프트웨어 -> 코드 재사용성, 모듈화, 확장성과 유연성, 추상화를 통한 복잡도 관리, 유지보수 -> 여러명이 함께 만들기 용이

*클래스가 필요한 이유?
학생을 추가하려면 여러부분을 수정하고 여러번 수정해야함, 배열 사용의 한계(인덱스 순서를 항상 정확히 맞추어야함)
-> '학생'이라는 개념을 하나로 묶어서 관리하고 싶어짐 -> 클래스 도입

*클래스에 정의한 변수들 -> 멤버변수, 필드
 - 멤버변수 : 특정 클래스에 소속된 멤버이기 때문.
 - 필드 : 데이터 항목을 가르키는 전통적인 용어. 데이터베이스, 엑셀 등에서 데이터 각각의 항목을 필드라 함.

*타입은 데이터의 종류나 형태를 나타냄
*사용자 정의 타입을 만드려면 설계도가 필요함 -> 클래스 (붕어빵 틀)
*실제 메모리에 만들어진 실체를 객체 또는 인스턴스 (붕어빵)
*클래스를 통해서 사용자가 원하는 종류의 데이터 타입을 마음껏 정의할수 있게 됨.

*변수 선언
Student student1 //"Student타입"을 받을 수 있는 변수를 선언함

*객체 생성
student1 = new student() //객체를 생성할 때마다 각자 다른 참조값이 생성됨

*참조값을 변수에 보관하는 이유
생성된 객체에 접근할수 있는 방법이 없다.(집샀는데 주소를 안알려줌)->저장된 참조값(주소)를 통해 메모리에 존재하는 객체에 접근할수 있게 됨

*객체를 사용하려면 .(점,dot)를 사용
student1.name="학생1"

*객체vs인스턴스
 - 둘다 클래스에서 나온 실체
 - 인스턴스: 인스턴스라는 단어를 쓴다는 것은 클래스와의 관계를 좀 더 강조할때 사용(student1은 Student클래스의 인스턴스다)

*new를 쓴다는 것은 메모리상에 무언가의 공간을 만듦. -> 참조값을 반환

*배열 생성
Student students = new Student[2]

**자바에서 대입(=)은 항상 변수에 들어있는 값을 복사해서 전달한다.
기본형이면 변수에 들어있는 실제 값을 복사해서 대입
참조형이면 변수에 들어있는 참조값(객체의 위치를 가르키는 주소값)을 복사해서 대입

*배열을 도입하면 향상된 for문으로 최적화 하기 쉬움.

*변수의 데이터 타입을 크게보면 "기본형"과 "참조형"으로 분류함.

*기본형: int, long, double, boolean, 자바가 기본으로 제공하는 데이터 타입.

*참조형
- Student student1, int[] students, 데이터에 접근하기 위한 "참조(주소, 위치)"를 저장하는 데이터 타입. 
- 객체 또는 배열에 사용. 기본형을 제외한 나머지는 모두 참조형이다.
- 객체는 .(dot)을 통해 찾아감
- 배열은 []를 통해 찾아감

*기본형 vs 참조형
 - 기본형은 연산이 가능하지만, 참조형은 "위치"만 갖고있기 때문에 계산에 사용할 수 없음.

*String은 사실 클래스다. 따라서 참조형. 기본형처럼 문자 값을 바로 대입 가능. 문자를 자주 다루기 때문에 자바에서 특별하게 편의기능 제공.

*메서드의 매개변수는 항상 값에 의해 전달됨 (그 값이 실제 값이냐, 참조(주소)값이냐에 따라 동작이 달라짐!)

*메서드로 기본형 데이터를 전달: 해당 "값"이 복사되어 전달 -> 메서드 내부에서 파라미터의 값을 변경해도 호출자의 변수 값에는 영향 없음.
*메서드로 참조형 데이터를 전달: "참조값(주소)"이 복사되어 전달 -> 메서드 내부에서 전달된 객체의 파라미터를 변경하면 전달된 객체도 변경됨.

*메서드 호출
Student student1 = new Student();
printStudent(student1);

static void printStudent(Student student){
System.out.println("이름:" + student.name + "나이:" + student.age + "성적:" + student.grade);
}


*참조값을 통한 메서드 변경
static void initStudent(Student student, String name, int age, int grade){
student.name = name;
studen.age = age;
student.grad = grade;
}

*참조형은 메서드를 호출할 때 참조값을 전달함 -> 메서드 내부에서 전달된 참조값을 통해 객체의 값을 변경하거나, 값을 읽어서 사용할 수 있음.

*멤버변수(필드): 클래스에 선언
*지역변수: 메서드에 선언, 매개변수도 지역변수의 한 종류, 특정 지역(블록)에서만 사용되는 변수

*변수의 초기화
멤버변수: 자동 초기화, 인스턴스의 멤버변수는 인스턴스를 생성할때 자동으로 초기화됨. 숫자는0, boolean=false, 참조형=null, 개발자가 초기화를 직접 지정할 수 있다.
지역변수: 수동 초기화

*null : 가리키는 대상이 없거나, 가리키는 대상을 나중에 입력하고 싶을 때, null이라는 특별한 값을 넣어 둘 수 있음. (주소를 나중에 입력하고 싶어!!)

*GC(Garbage Collection) 아무도 참조하지 않는 인스턴스의 최후
ㄴ사용하지 않게 된 인스턴스를 예전엔 그대로 남아서 메모리를 차지했는데, 자바는 JVM(자바 가상 머신)이 GC를 자동으로 제거해 줌.

*JVM : Java Virtual Machine(자바 가상 머신)
-자바 프로그램을 실행하기 위한 가상의 컴퓨터 환경을 제공하는 역할을 함.
-자바 프로그램은 컴파일러에 의해 자바 바이트코드로 변환되고, 이 바이트코드는 JVM에서 실행됨.
-JVM은 이 바이트코드를 실제 기계어로 변환하고, 메모리 관리, 스레드 관리, 예외 처리 등의 작업을 수행하여 자바 프로그램의 실행을 지원함.
-JVM은 플랫폼 독립성을 제공함 (각 운영 체제(window, mac)에 맞게 구현)
-JVM은 자바 프로그램의 실행 환경을 관리함 (메모리 할당 및 회수, 가비지 컬렉션, 스레드 스케줄링, 예외 처리 등을 담당)
-JVM은 자바뿐만 아니라 다른 JVM 언어인 코틀린, 그루비 등의 프로그램도 실행할 수 있음.

*NullPointerException:
null 값을 가진 객체에 접근하려고 할 때 발생 (택배를 보냈는데 주소지가 없네?)
null값에 .(pointer, 점)를 찍어서 예외가 발생했다!ㅠㅠ

*NullPointerException 발생상황
null 값을 가진 객체의 메서드를 호출할 때
null 값을 가진 객체의 속성에 접근할 때
null 값을 가진 배열에 접근할 때

*NullPointerException 해결방법 예시
객체를 사용하기 전에 null인지 확인하거나, 조건문을 사용하여 null인 경우에 대한 처리를 추가합니다.
null을 허용하지 않는 변수나 매개변수를 사용하도록 설계합니다.
객체를 초기화할 때 null이 아닌 유효한 값으로 초기화합니다.
안전한 호출(?.)이나 엘비스 연산자(?:)를 사용하여 null-safe한 코드를 작성합니다.

*절차지향 프로그래밍 : 실행 순서를 중요하게 여기는 방식
*객체지향 프로그래밍 : 객체들 간의 상호작용을 중심으로 프로그래밍하는 방식.
*모듈화 : 큰 시스템이나 프로젝트를 작은 단위로 분할하여 각각을 독립적으로 개발하고 관리하는 것.
(레고 블럭. 필요한 블럭을 가져다 꼽아서 사용. 조립해서 프로그램을 만듦)

*static이 붙으면 객체를 생성하지 않고도 메서드를 호출할 수 있다.

*생성자: 객체를 생성한 직후 객체를 초기화 하기 위한 특별한 메서드로 생각할 수 있다.

**생성자의 이름은 클래스 이름과 같아야 한다. (첫글자도 대문자로)
**개발자가 직접 정의한 생성자는 반드시 생성자를 호출해야함 (반드시 입력해야해!)
*생성자는 반환타입이 없다. 비워두어야함
*나머지는 메서드와 비슷함.

*기본생성자: 매개변수가 없는 생성자
클래스에 생성자가 하나도 없으면? 자바 컴파일러는 매개변수가 없고 작동하는 코드가 없는 "기본생성자를 자동으로" 만들어줌(눈에 안보임)
생성자가 하나라도 있으면 자바는 기본생성자를 만들지 않는다.

*기본생성자를 왜 만들어줄까?
생성자 기능이 필요하지 않은 경우에도 개발자가 일일이 직접 정의해야하는 불편함이 생기기 때문에 자바가 편의기능을 제공함.

*this()의 규칙: this()는 생성자코드의 "첫 줄"에서만 작성 가능.

*접근제어자를 사용하면 해당 클래스 외부에서 특정 필드나 메서드에 접근하는 것을 허용하거나 제한할 수 있다.

*private: 모든 외부 호출을 막는다.
*default(package-private): 같은 패키지 안에서 호출은 허용한다. (아무것도 안적으면 기본값)
*protected: 같은 패키지 안에서 호출은 허용한다. 패키지가 달라도 상속관계의 호출은 허용한다.
*public: 모든 외부 호출을 허용한다.

*차단 private -> default -> protected -> public 허용

*클래스 레벨의 접근 제어자 규칙 : public, default 두개만 사용할 수 있다.
*public 클래스는 반드시 파일명과 이름이 같아야 한다.
*하나의 자바파일에 하나의 public클래스만 등장할 수 있음.

*캡슐화:  관련된 데이터와 메서드를 하나의 단위로 묶어 외부에는 꼭 필요한 기능만 노출하고 나머지는 모두 내부로 숨기는 것.
- 객체에는 속성과 기능이 있다. 가장 필수로 숨겨야 하는것은 속성(데이터)이다. (클래스 안에서 데이터를 다루는 모든 로직을 무시하고 외부에서 데이터에 접근하여 변경될 수 있기 때문)
- 따라서, 객체의 데이터는 객체가 제공하는 기능인 메서드를 통해서 접근해야 한다.
- 객체의 기능 중에서 외부에서 사용하지 않고 내부에서만 사용하는 기능들이 있다 -> 이런 기능도 감추는 것이 좋다. (외부에서 꼭 필요한 것만 외부에 노출하자)
- 정리하면, 데이터는 모두 숨기고, 기능은 꼭 필요한 기능만 노출하는 것이 좋은 캡슐화 이다.


*자바 메모리 구조는 크게 메서드 영역, 스택 영역, 힙 영역 3개로 나눌 수 있다.

*메서드 영역: 프로그램을 실행하는데 필요한 공통 데이터를 관리함. 이 영역은 프로그램의 모든 영역에서 공유함.
-클래스정보: 클래스의 실행 코드(바이트코드), 필드, 메서드와 생성자 코드 등 모든 실행 코드가 존재함.
-static영역: static변수들을 보관함.
-런타임상수 풀: 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관함. (예를 들어 "hello")

*스택 영역: 자바 실행 시, 하나의 실행 스택이 생성됨. 각 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함함.
-스택 프레임: 메서드를 호출할 때 마다 하나의 스택 프레임이 쌓이고 메서드가 종료되면 해당 스택 프레임이 제거됨.
-후입선출(LIFO)방식으로 동작함: 나중에 넣은 것이 가장 먼저 나오는 것. (예를 들어, 편의점의 음료수 정렬)

*힙 영역: 객체(인스턴스)와 배열이 생성되는 영역임. 가비지 컬렉션(GC)이 이루어지는 주요 영역이며, 더이상 참조되지 않는 객체는 GC에 의해 제거됨.

*인스턴스가 생성되더라도 공통된 클래스로부터 생성된 메서드는 공통된 코드를 공유하므로 이런한 메서드는 메서드 영역에서 관리되고 실행된다. 
정리하면, 인스턴스의 메서드를 호출하면 실제로는 메서드 영역에 있는 코드를 불러서 수행함.



*멤버변수는 2가지로 분류할수 있어
-인스턴스 변수: 인스턴스를 생성해야 사용할 수 있음. 인스턴스에 소속되어 있음. 따라서 인스턴스변수라 함. 인스턴스 변수는 인스턴스를 만들때마다 새로 만들어짐.
-클래스 변수(static): 클래스변수, 정적변수, static변수 등으로 부름. static이 붙은 멤버변수는 클래스에 바로 접근해서 사용 가능. 클래스 자체에 소속되어 있어서 클래스 변수라고함. 클래스변수는 자바프로그램을 시작할때 딱 1개 만들어짐. 인스턴스와는 다르게 보통 여러곳에서 공유하는 목적으로 사용됨. (예시로 인스턴스 하나씩 만들때마다 count의 값이 하나씩 증가 하게 하고싶어! -> 공용변수(static)생성 하기.)




*변수와 생명주기
-지역 변수(매개변수 포함) : 스택 영역에 생존 -> 메서드가 종료되면 함께 제거됨, 생존 주기가 짧다
-인스턴스 변수 : 힙 영역에 생존 -> GC가 발생하기 전까지는 생존함
-클래스 변수: 메서드 영역에 생존 -> 메서드영역은 공용공간이므로 JVM이 로딩되는 순간 생성되고 JVM이 종료될 때까지 생명주기가 이어짐. 가장 긴 생명주기 -> 말 글대로 정적임.

*인스턴스를 통한 static접근은 추천하지 않음. 코드를 읽을때 마치 인스턴스 변수에 접근하는것 처럼 오해할 수 있기 때문. -> 따라서 "클래스를 통한 static접근을 하는것이 더 명확하다."



*static메서드 내부에서는 인스턴스 변수와 인스턴스 메서드를 사용할 수 없다. (static영역에서는 힙영역에있는 것 들을 참조할 수 있는 값이 없어서)
*단, 물론 "클래스(참조값)을 통한" 인스턴스 변수와 인스턴스 메서드는 사용할 수 있어.



*멤버 메서드의 종류
인스턴스 메서드: static이 붙지 않은 멤버 메서드, 인스턴스를 생성해야 사용할수 있음. 인스턴스 소속.
클래스 메서드: static이 붙은 멤버 메서드 (정적 메서드, 클래스 메서드, static 메서드 등으로 불림). 인스턴스와 무관하게 클래스에 바로 접근해서 사용 가능. 클래스 자체에 소속되어 있음.


*정적 메서드의 활용
주로 객체 생성이 필요 없이 메서드의 호출 만으로 필요한 기능을 수행할 때 주로 사용. 
메서드 하나로 끝나는 유틸리티성 메서드에 자주 사용. 입력한 값을 계산하고 반환하는 것이 대부분.

*정적 메서드를 많이 사용할 때, 클래스 명을 매번 쓰기엔 번거로울수 있다. 이 때 "임포트"하면 더 간편히 메서드를 호출해서 사용할 수 있다. (이런 기능이 있다 정도)


*public static void main()
- 메인 메서드는 static 이기 때문에 정적 메서드 이다. 따라서 객체를 생성하지 않아도 main() 메서드가 작동 했다.
- 정적 메서드는 같은 클래스 내부에서의 정적 메서드만 호출할 수 있다. 따라서 메인 메서드에서 다른 메서드를 호출하려면 정적 메서드(static)를 선언해서 사용해야 함.

*final : 더는 값을 변경할 수 없어! -> kotlin의 val인가?ㅎㅎ
-최초 한번만 값 할당 가능.

*final 예시
final int data2 = 10;
static void method(final int parameter) {} // 메서드 내부에서 매개변수의 값을 변경할 수 없음.(메서드 내부에서 prameter=20; 불가능)
생성자를 통한 final int value; // 처음 생성되었을때 정해지면 나중에 못바꿈.
static final int CONST_VALUE = 10; // static final이 붙으면 상수이므로 관례상 이름을 대문자로 써야해.

*final 필드를 필드에서 초기화 하는 경우 공통된 값을 가지게된다-> 모든 인스턴스들이 공통된 값을 가짐 (메모리 낭비, 중복) -> static을 붙이면 static영역으로 가게되어 메모리 절약 -> 따라서 "final + 필드초기화"를 사용하는 경우 static을 붙여서 사용하는것이 효과적임.


*자바 상수의 특징 (Kotlin의 val 같은뎁 ㅎ_ㅎ..)
static final 키워드를 사용.
대문자를 사용하고 구분은 _(언더스코어)로 함 -> 관례
상수는 기능이 아니라 고정된 값 자체를 사용하는 것이 목적임.
상수는 값을 변경할 수 없다. 따라서 필드에 직접 접근해도 데이터가 변하는 문제가 발생하지 않음!
애플리케이션 전반에 사용되는 경우가 많은데 때문에 public을 자주 사용함.
물론 특정 위치에서만 사용된다면 접근 제어자를 사용하면 된다.
상수는 중앙에서 값을 하나로 관리할 수 있다는 장점이 있다.
상수는 런타임에 변경할 수 없다. 변경하고 싶다면 프로그램을 종료하고, 코드를 변경한 다음에 다시실행 해야 한다.

*final 참조 대상의 값은 변경 가능
final Data data = new data();
data.value = 10; //참조 대상의 값은 변경 가능

*상속 // public class Child extends Parent
상속은 객체지향 프로그래밍의 핵심 요소 중 하나임.
기존 클래스의 속성과 기능을 물려받는 것.
사용하려면 extends 키워드 사용.
extends 대상은 하나만 선택 할수 있음.
public class ElectricCar extends Car {} //전기차는 차에게 상속됨.

*부모클래스(슈퍼클래스): 상속을 통해 자신의 필드와 메서드를 다른 클래스에 제공하는 클래스.
*자식클래스(서브클래스): 부모 클래스로 부터 필드와 메서드를 상속받는 클래스. 

*다중 상속 불가함: 계층 구조가 매우 복잡해 질수 있기 때문.

*상속이라고해서 단순히 필드와 메서드만 물려받는것이 아님. 외부에서 볼때는 하나의 인스턴스를 생성하는것 같지만, 내부에서는 부모와 자식이 모두 생성되고 공간도 구분됨.
*상속관계의 객체를 호출할 때, 대상 타입을 정해야 함. 이때 호출자의 타입을 통해 대상 타입을 찾는다.
*현재 타입에서 기능을 찾지 못하면 상위 부모 타입으로 기능을 찾아서 실행함. (기능을 찾지 못하면 컴파일 오류 발생)

*오버라이딩: 부모에게서 상속받은 기능을 자식이 재정의 하는것. (@Override)
@Override //오버라이딩을 하려면 Override 애노테이션을 써줌 //쓰지않아도 오류는 나지 않지만, 좀 더 명확히 볼수있도록 쓰는게 좋앙 ^^!
public void move() {
System.out.println("전기차를 빠르게 이동합니다.");
}

*오버로딩과 오버라이딩 구분하기~~@! (실무에서 둘 다 사용)

*오버로딩: 번역하면 과적. 과하게 물건을 담음. 같은이름의 메서드를 여러개 정의함.
*오버라이딩: 번역하면 재정의. 기존 기능을 새로운 기능으로 덮는다! 부모의 기능을 자식이 재정의 하는 것.

*메서드 오버라이딩의 조건 (간단히 ,, 부모메서드와 같은메서드를 오버라이딩 할 수 있다 정도로 이해하면 충분해^^)
-메서드 이름 같아야 함.
-메서드의 파라미터 타입, 순서, 개수가 같아야 함
-반환 타입이 같아야 함.
-접근제어자는 상위클래스보다 더 제한적이어서는 안됨.(차단 private -> default -> protected -> public 허용)
-예외: 오버라이딩 메서드는 상위 클래스의 메서드보다 더 많은 체크 예외를 throw로 선언할 수 없다. (나중에 배워)
-static, final, private 붙은 메서드는 오버라이딩 될 수 없다.(static: 그냥 필요하때 호출하면 되는데 오버라이딩이 의미없음, final: 재정의 금지, private: 해당 클래스만 접근가능하므로 하위클래스에서 사용불가)
-생성자는 오버라이딩 할 수 없음.

*UML표기법
+ : public
# : protected (패키지가 달라도 상속관계에서 사용할 수 있음!)
~ : default
- : private

*super
부모클래스와 자식클래스의 필드 이름과 메서드 "이름이 같지만" super를 사용해 부모클래스에 있는 기능을 사용할 수 있다. // super.hello();

**상속관계를 사용하면 자식클래스의 생성자에서 부모클래스의 생성자 super(...) 를 반드시 호출해야 한다!! (단, 기본생성자인 경우 생략가능해서 안보여지게 되는것)
*상속관계의 생성자 호출은 결과적으로 부모에서 자식 순서로 실행된다. 따라서 부모의 데이터를 먼저 초기화하고, 그다음에 자식의 데이터를 초기화한다.
*생성자에 this를 써야 하더라도 언젠가는 super(...)를 꼭!! 생성자를 만들어 줘야함. 

*final이 붙은 클래스는 상속 불가.



*다형성(Polymorphism): 이름 그대로  "다양한 형태", "여러 형태"
: 보통 하나의 객체는 하나의 타입으로 고정되어 있다. 그런데 다형성을 사용하면 하나의 객체가 다른 타입으로 사용될 수 있다.

*부모 타입의 변수가 자식 인스턴스를 참조한다 

*부모 타입은 자식 타입을 담을 수 있다. Parent poly = new Child() //성공
*반대로 자식 타입은 부모 타입을 담을 수 없다. Child child1 = new Parent() //컴파일 오류 발생
*단 자식의 기능은 호출할 수 없다. poly.childMethod(); //컴파일 오류 발생

**다형적 참조의 핵심은 부모는 자식을 품을 수 있다는 것이다!!

 

*다운캐스팅 (부모타입 -> 자식타입)
Parent poly = new Child()
Child child = (Child) poly; // 부모타입인 poly를 -> (Child)를 사용해 자식타입으로 강제로 바꿔서 왼쪽변수에 대입할수 있게 하는 것. -> 따라서 자식의 기능인 poly.childMethod() 를 호출할 수 있게 됨
(타입)처럼 괄호와 그 사이에 타입을 지정하면 참조대상을 특정 타입으로 변경할 수 있다.

*단 poly의 타입은 Parent로 기존과 같이 유지된다!!

*일시적 다운캐스팅 - 해당 메서드를 호출하는 순간만 다운캐스팅
((Child)poly).childMethod(); //연산자 우선순위에 따라 다운캐스팅을 먼저 실행되도록 괄호 사용.




*업캐스팅 (자식타입 -> 부모타입) //매우 자주 사용함.

Child child = new Child();
Parent parent1 = (Parent) child; // 업캐스팅은 생략 가능, 생략 권장 -> Parent parent1 = child; //가능

parent1.parentMethod(); //가능
parent2.parentMethod(); //가능

*다운 캐스팅이 불가능한 경우: 애초에 부모 타입으로 생성된 경우 불가함. (생성될때 메모리상에 부모타입만 생성되고, 하위타입의 메모리가 생성되지 않음) -> 자식타입의 메모리가 없어서 다운캐스팅을 할수 없음.
반면에 업캐스팅은 : 자식타입이 생성됬을때 부모 타입들이 모두 메모리에 생성되므로 -> 상위로 올라가는 업캐스팅은 문제가 되지 않음. -> 그래서 자바에서 업캐스팅은 모두 허용 ^^


*instanceof 연산자: 인스턴스의 타입을 확인 (참조하는 대상이 다양한데 어떤 인스턴스를 참조하고 있는지 알려줘~~!)
타입을 왜 확인해?-> 잘못된 다운캐스팅을 할 경우 심각한 런타임 오류가 발생할 수도 있기 때문에 꼭 타입을 확인해 봐야해 ㅠㅠ
true/false를 반환함. 



*자바16이후: instanceof를 사용함과 동시에 변수를 선언하면서 다운캐스팅을 바로 할수 있음. // parent instanceof Child child

**메서드 오버라이딩: 오버라이딩 된 메서드는 항상 우선권을 가진다. (부모 타입이 메서드를 호출할 때, 생성된 인스턴스에 자식타입의 메모리를 가지고 있으면 오버라이딩된 자식타입의 메서드가 우선권을 가짐!!)


*다형성의 위력
- 다형적 참조: 하나의 변수 타입으로 다양한 자식 인스턴스를 참조할 수 있는 기능
- 메서드 오버라이딩: 기존 기능을 하위 타입에서 새로운 기능으로 재정의













'📒 TIL - Today I Learned' 카테고리의 다른 글

20231227 / Todolist  (0) 2023.12.27
20231226 / 오늘은 뭔가  (2) 2023.12.26
20231223 / 이번 주말은 ㅈㅂ  (0) 2023.12.23
20231222 / 현타  (2) 2023.12.22
20231221 / Repository Layer / Database / DBMS / SQL  (0) 2023.12.21

블로그의 정보

꿈틀꿈틀 개발일기

jeongminy

활동하기