깃허브 링크: https://github.com/2020-byte/java-calculator
JavaScript를 주로 사용하다가 Java를 배우면서 발견한 흥미로운 점은 같은 패키지 내의 클래스들은 import 문이 필요없다는 것입니다!
// /src/models/User.js
export class User {
// ...
}
// /src/models/UserService.js
import { User } from './User.js' // 같은 폴더에 있어도 import 필요!
class UserService {
createUser() {
return new User();
}
}
// /src/models/User.java
package models;
public class User {
// ...
}
// /src/models/UserService.java
package models;
// User 클래스를 import 할 필요가 없음!
public class UserService {
public User createUser() {
return new User(); // 같은 패키지라서 바로 사용 가능
}
}
오늘 Java의 Stream API를 배우면서 JavaScript의 Array 고차 함수들과 매우 유사하다는 것을 발견했다.
이는 함수형 프로그래밍의 영향을 받은 것으로 보인다.
JavaScript:
const numbers = [1, 2, 3, 4, 5];
const result = numbers
.filter(n => n % 2 === 0) // 짝수 필터링
.map(n => n * 2) // 2배로 변환
.reduce((sum, n) => sum + n, 0); // 합계
console.log(result); // 12
Java:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int result = numbers.stream()
.filter(n -> n % 2 == 0) // 짝수 필터링
.map(n -> n * 2) // 2배로 변환
.reduce(0, (sum, n) -> sum + n); // 합계
System.out.println(result); // 12
메서드 체이닝
람다식 사용
n => n * 2
n -> n * 2
불변성
스트림 생성이 필요함
.stream()
메서드로 스트림 생성 필요기본형 특화 스트림
IntStream.range(1, 6) // 1~5 숫자 생성
.filter(n -> n % 2 == 0)
.sum();
병렬 처리 지원
numbers.parallelStream()
.filter(n -> n % 2 == 0)
.map(n -> n * 2)
.reduce(0, Integer::sum);
두 언어의 유사성을 발견하면서, 함수형 프로그래밍의 개념이 현대 프로그래밍 언어에 얼마나 큰 영향을 미쳤는지 알게 되었다. JavaScript를 사용해서 Java의 스트림 API는 매우 친숙하게 다가왔으며, 특히 병렬 처리 같은 추가 기능은 매우 인상적이었다.
제네릭은 클래스, 인터페이스, 메소드에서 사용할 타입을 파라미터화하는 방법입니다. 이를 통해 코드의 재사용성을 높이고 타입 안정성을 보장할 수 있습니다.
// 클래스에서의 사용
class Container<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
setValue(value: T): void {
this.value = value;
}
}
// 인터페이스에서의 사용
interface Pair<T, U> {
first: T;
second: U;
}
// 함수에서의 사용
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
// 실제 사용 예시
const numberContainer = new Container<number>(42);
const stringPair: Pair<string, number> = { first: "Hello", second: 42 };
const swapped = swap<string, number>(["Hello", 42]); // [42, "Hello"]
// 클래스에서의 사용
public class Container<T> {
private T value;
public Container(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
// 인터페이스에서의 사용
public interface Pair<T, U> {
T getFirst();
U getSecond();
void setFirst(T first);
void setSecond(U second);
}
// 메소드에서의 사용
public static <T, U> Pair<U, T> swap(Pair<T, U> pair) {
return new Pair<>(pair.getSecond(), pair.getFirst());
}
// 실제 사용 예시
Container<Integer> numberContainer = new Container<>(42);
Pair<String, Integer> stringPair = new Pair<>("Hello", 42);
// 단일 타입 제약
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // length 프로퍼티 사용 가능
return arg;
}
// 복수 타입 제약
interface HasName {
name: string;
}
interface HasAge {
age: number;
}
function printPersonInfo<T extends HasName & HasAge>(person: T): void {
console.log(`${person.name} is ${person.age} years old`);
}
// 키 타입 제약
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// 상한 경계
public <T extends Number> double sumNumbers(List<T> numbers) {
return numbers.stream()
.mapToDouble(Number::doubleValue)
.sum();
}
// 하한 경계
public void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
// 다중 경계
public <T extends Number & Comparable<T>> T findMax(List<T> list) {
return list.stream()
.max(T::compareTo)
.orElseThrow();
}
type NonNullable<T> = T extends null | undefined ? never : T;
// 사용 예시
type ResultType = NonNullable<string | null | undefined>; // string
// 분배 조건부 타입
type ToArray<T> = T extends any ? T[] : never;
type StrNumArr = ToArray<string | number>; // string[] | number[]
// 모든 프로퍼티를 읽기 전용으로 만들기
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// 모든 프로퍼티를 선택적으로 만들기
type Partial<T> = {
[P in keyof T]?: T[P];
};
// 실제 사용 예시
interface User {
name: string;
age: number;
}
type ReadonlyUser = Readonly<User>;
type PartialUser = Partial<User>;
// PECS (Producer Extends, Consumer Super) 원칙
public class Collections {
// Producer - extends
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i));
}
}
// Consumer - super
public static <T> void addAll(Collection<? super T> collection, T... elements) {
for (T element : elements) {
collection.add(element);
}
}
}
// 제네릭 리포지토리 패턴
interface Repository<T> {
findById(id: string): Promise<T>;
findAll(): Promise<T[]>;
create(item: T): Promise<T>;
update(id: string, item: Partial<T>): Promise<T>;
delete(id: string): Promise<void>;
}
// 구현 예시
class UserRepository implements Repository<User> {
async findById(id: string): Promise<User> {
// 구현
}
async findAll(): Promise<User[]> {
// 구현
}
async create(user: User): Promise<User> {
// 구현
}
async update(id: string, user: Partial<User>): Promise<User> {
// 구현
}
async delete(id: string): Promise<void> {
// 구현
}
}
// 제네릭 DAO 패턴
public interface GenericDao<T, ID> {
Optional<T> findById(ID id);
List<T> findAll();
T save(T entity);
void delete(ID id);
boolean exists(ID id);
}
// 구현 예시
@Repository
public class UserDao implements GenericDao<User, Long> {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public Optional<User> findById(Long id) {
// 구현
}
@Override
public List<User> findAll() {
// 구현
}
@Override
public User save(User user) {
// 구현
}
@Override
public void delete(Long id) {
// 구현
}
@Override
public boolean exists(Long id) {
// 구현
}
}
소규모 프로젝트
대규모 프로젝트
VS Code에서 JavaScript 프로젝트만 하다가 처음으로 IntelliJ에서 Java 프로젝트를 시작하게 되었습니다. IDE와 언어가 달라지니 git을 이용하는 것이 헷갈렸습니다.
프로젝트를 생성하고 Git으로 관리하려는데, Java 프로젝트의 경우 어떤 파일들을 .gitignore에 넣어야 하는지 알지 못했습니다.
프로젝트 디렉토리를 살펴보니 IntelliJ가 이미 .gitignore 파일을 자동으로 생성해놓은 것을 발견했습니다. 파일 내용을 보니 .idea/, *.iml, out/ 등 여러 파일과 디렉토리가 이미 정의되어 있었습니다. 이 파일을 바탕으로 검색해보니 각각이 IDE 설정 파일, 프로젝트 모듈 파일, 빌드 결과물 등을 의미한다는 것을 알 수 있었습니다.
GitHub 리포지토리 연결을 위해 검색하던 중 IntelliJ의 Git 메뉴에서 'Create Repository'를 발견했습니다. 이 기능을 사용하니 GitHub의 기존 리포지토리를 선택하거나 새로 만들 수 있었고, 간단한 설정만으로 바로 연결할 수 있었습니다.
최종적으로 다음과 같은 워크플로우를 정립했습니다:
1. 프로젝트 초기 설정:
이러한 경험을 통해 새로운 개발 환경에서도 제공되는 기능들을 잘 활용하면 오히려 더 효율적으로 작업할 수 있다는 것을 깨닫게 되었습니다.