JS와 비교하며 자바와 익숙해지기(계산기 만들기)

하기·2024년 11월 13일
0

깃허브 링크: https://github.com/2020-byte/java-calculator

목차

1. JavaScript vs Java 패키지 시스템 비교

  • 🤔 발견한 점
  • 🎯 JavaScript vs Java 비교
  • 💡 주요 포인트
  • 🌟 실무적 의미
  • ⚠️ 주의할 점

2. Java Stream API vs JavaScript Array 메서드 비교

  • 💡 발견한 점
  • 🔍 코드 비교
    • 기본적인 체이닝 예제
  • 🎯 주요 유사점
  • 🌟 Java Stream의 특별한 점
  • 🎓 얻어가는 배움

3. TypeScript vs Java 제네릭 심화 분석

  • 제네릭의 기본 개념과 용도
  • 타입 제약 심화
  • 고급 기능 비교
  • 실전 사용 사례
  • 성능과 최적화
  • 주의사항과 Best Practices
  • 실무적 차이점

4. IntelliJ에서의 첫 Java 프로젝트 Git 연동 트러블슈팅

  • 배경
  • 발단
  • 전개
  • 위기
  • 절정
  • 결말

JavaScript vs Java 패키지 시스템 비교

🤔 발견한 점

JavaScript를 주로 사용하다가 Java를 배우면서 발견한 흥미로운 점은 같은 패키지 내의 클래스들은 import 문이 필요없다는 것입니다!

🎯 JavaScript vs Java 비교

JavaScript의 경우

// /src/models/User.js
export class User {
    // ...
}
// /src/models/UserService.js
import { User } from './User.js'  // 같은 폴더에 있어도 import 필요!

class UserService {
    createUser() {
        return new User();
    }
}

Java의 경우

// /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();  // 같은 패키지라서 바로 사용 가능
    }
}

💡 주요 포인트

  1. Java에서는 같은 패키지 내의 클래스들은 자동으로 사용 가능
  2. 이는 패키지를 하나의 모듈로 보는 Java의 설계 철학을 반영
  3. JavaScript의 모듈 시스템은 파일 단위로 동작하는 반면, Java는 패키지 단위로 동작

🌟 실무적 의미

  • 큰 프로젝트에서 import 문이 줄어들어 코드가 깔끔해짐
  • 같은 패키지 내 클래스들은 자연스럽게 응집도가 높아지는 효과
  • 패키지 구조 설계가 더욱 중요해짐

⚠️ 주의할 점

  • 다른 패키지의 클래스는 여전히 import 필요
  • protected 멤버는 같은 패키지 내에서만 접근 가능
  • 패키지 이름은 소문자로 작성하는 것이 관례

Java Stream API vs JavaScript Array 메서드 비교

💡 발견한 점

오늘 Java의 Stream API를 배우면서 JavaScript의 Array 고차 함수들과 매우 유사하다는 것을 발견했다.
이는 함수형 프로그래밍의 영향을 받은 것으로 보인다.

🔍 코드 비교

1. 기본적인 체이닝 예제

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

🎯 주요 유사점

  1. 메서드 체이닝

    • 두 언어 모두 메서드를 연속적으로 호출 가능
    • 코드의 가독성과 유지보수성이 향상됨
  2. 람다식 사용

    • JS: n => n * 2
    • Java: n -> n * 2
    • 거의 동일한 문법!
  3. 불변성

    • 원본 데이터를 변경하지 않고 새로운 결과를 반환
    • 함수형 프로그래밍의 핵심 개념 반영

🌟 Java Stream의 특별한 점

  1. 스트림 생성이 필요함

    • JS: 배열 메서드를 바로 사용
    • Java: .stream() 메서드로 스트림 생성 필요
  2. 기본형 특화 스트림

    IntStream.range(1, 6)  // 1~5 숫자 생성
            .filter(n -> n % 2 == 0)
            .sum();
  3. 병렬 처리 지원

    numbers.parallelStream()
           .filter(n -> n % 2 == 0)
           .map(n -> n * 2)
           .reduce(0, Integer::sum);

🎓 얻어가는 배움

두 언어의 유사성을 발견하면서, 함수형 프로그래밍의 개념이 현대 프로그래밍 언어에 얼마나 큰 영향을 미쳤는지 알게 되었다. JavaScript를 사용해서 Java의 스트림 API는 매우 친숙하게 다가왔으며, 특히 병렬 처리 같은 추가 기능은 매우 인상적이었다.


TypeScript vs Java 제네릭 심화 분석

1. 제네릭의 기본 개념과 용도

1.1 제네릭이란?

제네릭은 클래스, 인터페이스, 메소드에서 사용할 타입을 파라미터화하는 방법입니다. 이를 통해 코드의 재사용성을 높이고 타입 안정성을 보장할 수 있습니다.

1.2 기본 문법 상세 비교

TypeScript의 제네릭

// 클래스에서의 사용
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"]

Java의 제네릭

// 클래스에서의 사용
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);

2. 타입 제약 심화

2.1 TypeScript의 타입 제약

// 단일 타입 제약
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];
}

2.2 Java의 타입 제약

// 상한 경계
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();
}

3. 고급 기능 비교

3.1 TypeScript의 고급 기능

조건부 타입

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>;

3.2 Java의 고급 기능

와일드카드 타입 심화

// 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);
        }
    }
}

4. 실전 사용 사례

4.1 TypeScript 실전 예제

// 제네릭 리포지토리 패턴
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> {
        // 구현
    }
}

4.2 Java 실전 예제

// 제네릭 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) {
        // 구현
    }
}

5. 성능과 최적화

5.1 TypeScript

  • 컴파일 시점에만 타입 체크
  • 런타임에는 일반 JavaScript로 변환되어 실행
  • 타입 정보는 개발 시점에만 유용
  • 번들 크기에 영향을 주지 않음

5.2 Java

  • 타입 소거로 인한 런타임 오버헤드 최소화
  • 제네릭 배열 생성 불가 (타입 안전성 보장)
  • Bridge 메소드 생성으로 인한 약간의 메모리 사용
  • 컴파일된 바이트코드 크기 증가 없음

6. 주의사항과 Best Practices

6.1 TypeScript

  1. 과도한 제네릭 사용 피하기
  2. 명시적 타입 선언보다 타입 추론 활용
  3. 유니온 타입과 조건부 타입 적절히 활용
  4. 제네릭 제약 조건 명확히 정의

6.2 Java

  1. PECS 원칙 준수
  2. 불필요한 타입 매개변수 제거
  3. Raw 타입 사용 피하기
  4. 타입 소거 고려한 설계

7. 실무적 차이점

7.1 프로젝트 규모별 선택

  • 소규모 프로젝트

    • TypeScript: 빠른 개발과 유연성
    • Java: 엄격한 타입 체크와 안정성
  • 대규모 프로젝트

    • TypeScript: 점진적 타입 시스템의 이점
    • Java: 강력한 타입 시스템과 리팩토링 용이성

7.2 팀 구성에 따른 고려사항

  • TypeScript
    • JavaScript 개발자의 쉬운 적응
  • Java
    • 엄격한 타입 체크로 팀 협업 용이

IntelliJ에서의 첫 Java 프로젝트 Git 연동 경험

배경

VS Code에서 JavaScript 프로젝트만 하다가 처음으로 IntelliJ에서 Java 프로젝트를 시작하게 되었습니다. IDE와 언어가 달라지니 git을 이용하는 것이 헷갈렸습니다.

발단

프로젝트를 생성하고 Git으로 관리하려는데, Java 프로젝트의 경우 어떤 파일들을 .gitignore에 넣어야 하는지 알지 못했습니다.

전개

프로젝트 디렉토리를 살펴보니 IntelliJ가 이미 .gitignore 파일을 자동으로 생성해놓은 것을 발견했습니다. 파일 내용을 보니 .idea/, *.iml, out/ 등 여러 파일과 디렉토리가 이미 정의되어 있었습니다. 이 파일을 바탕으로 검색해보니 각각이 IDE 설정 파일, 프로젝트 모듈 파일, 빌드 결과물 등을 의미한다는 것을 알 수 있었습니다.

위기와 해결

GitHub 리포지토리 연결을 위해 검색하던 중 IntelliJ의 Git 메뉴에서 'Create Repository'를 발견했습니다. 이 기능을 사용하니 GitHub의 기존 리포지토리를 선택하거나 새로 만들 수 있었고, 간단한 설정만으로 바로 연결할 수 있었습니다.

결말

최종적으로 다음과 같은 워크플로우를 정립했습니다:
1. 프로젝트 초기 설정:

  • IntelliJ가 자동 생성한 .gitignore 활용해서 불필요한 파일 관리 제외
  • Git 메뉴의 'Create Repository'로 GitHub 연결
  1. 이후 Git 작업:
  • git add, commit, push 등 익숙한 터미널 명령어로 작업

이러한 경험을 통해 새로운 개발 환경에서도 제공되는 기능들을 잘 활용하면 오히려 더 효율적으로 작업할 수 있다는 것을 깨닫게 되었습니다.

0개의 댓글