Computer Science/기타 CS

데이트팝 기술면접 풀이

JM Lee 2024. 3. 4. 16:56
728x90

https://blog.datepop.co.kr/python-interview/

 

데이트팝 Python 주니어 개발자 인터뷰 후기

데이트팝 Python 주니어 개발자 인터뷰 후기

blog.datepop.co.kr

 

위 블로그에서 기술면접 인터뷰 후기를 보게 되었고,

데이트팝 면접 질문들에 관해 스스로 공부했던 것을 대답하기로 했다.

 

비록 도달하진 못했지만 내가 지금 어느 정도인지도 확인할 필요가 있었는데,

생각보다 합격의 길이 먼듯 멀지 않은듯..

한 걸음만 더 걸으면 되겠다고 생각했는데 왜 아직 모르는 게 있지?라는 생각을 면접 때마다 하게 되는 것 같다.

우선은 위 기술블로그를 보고 정확하게 질문에 대한 대답을 이해하기 위해 아래에 최선의 답을 적어보았다.

 


파이썬은 Garbage Collection는 메모리를 어떻게 관리할까?

파이썬은 동적으로 할당된 메모리를 관리하기 위해 Garbage Collection을 사용한다. 가비지 수집은 더 이상 사용되지 않는 메모리를 자동으로 해제하여 메모리 누수를 방지하고 프로그램의 성능을 향상시킨다. 파이썬은 참조 계수(reference counting)와 순환 가비지 수집(cyclic garbage collection)을 결합하여 메모리 관리를 수행한다.

참조 계수(reference counting): 파이썬은 모든 객체에 대해 참조 계수를 유지한다. 객체가 생성될 때마다 해당 객체의 참조 계수가 1 증가하고, 다른 객체가 해당 객체를 참조할 때마다 해당 객체의 참조 계수가 증가한다. 객체의 참조가 없어질 때마다 해당 객체의 참조 계수가 1씩 감소하고, 참조 계수가 0이 되면 해당 객체는 더 이상 사용되지 않으므로 메모리에서 해제된다.

순환 가비지 수집(cyclic garbage collection): 참조 계수만으로는 해제할 수 없는 순환 참조(circular reference)를 해결하기 위해 순환 가비지 수집이 사용된다. 순환 참조는 두 개 이상의 객체가 서로를 참조하여 참조 계수가 0이 되지 않아 메모리 누수를 발생시킬 수 있다. 파이썬의 순환 가비지 수집기는 이러한 순환 참조를 감지하고 해제하여 메모리를 회수한다.

이러한 메모리 관리 메커니즘은 대부분의 상황에서 효율적이지만, 가끔씩 순환 참조와 같은 특정 상황에서는 주의가 필요할 수 있다. 이런 경우에는 weak reference(약한 참조)나 직접 객체를 해제하는 방법 등을 사용하여 문제를 해결할 수 있다.

 


  • 아래 코드를 실행 시키면 출력이 어떻게 될까요? 그 이유도 같이 설명해주세요.
class A:
  def __init__(self):
    pass

  def __del__(self):
    print("deleted")

def main():
  a = A()
  print("end")

if __name__ == "__main__":
  # 출력이 어떻게 될까요?
  main()

 

answer

end
deleted

 

a = A() : class A의 객체를 만들어 a에 할당한다.

만약 여기서 가비지 컬렉션 특성을 몰랐다면 deleted가 먼저 호출되는 것이 아닌가?라는 생각을 했을 것이다.

그러나 이 특성을 가지고 있기 때문에 결과가 다르다. 아래 상세히 설명하자면

 

Python에서 객체가 생성되면 생성자인 __init__이 호출되고, 객체가 삭제되면 소멸자인 __del__이 호출된다.

그러나 Python의 가비지 컬렉션(Garbage Collection) 메커니즘은 객체의 소멸을 보장하지 않는다.

객체가 소멸될 때 __del__이 호출되는 것은 파이썬 인터프리터가 해당 객체의 메모리를 해제할 때 발생하는데, 이 때 발생하는 시점은 정확히 예측하기 어렵다.

 

만약 이대로면 "deleted"가 출력되지 않을 수도 있겠다는 생각을 했다.

 


class A:
	def __init__(self):
		pass

	def __del__(self):
		print("delete")

def main():
	a = A()
	del a
	print("end")

if __name__ == "__main__":
	# 출력이 어떻게 될까요?
	main()

 

answer

delete
end

 

del a 문이 실행될 때 객체 a의 참조가 삭제되고, 따라서 A 클래스의 인스턴스에 대한 참조가 없어지게 된다. 이로 인해 가비지 컬렉션의 작업이 시작되며, 객체의 소멸자 __del__이 호출되므로 "delete"를 "end"보다 먼저 출력한다.

 


# 아래 코드를 실행 시키면 출력이 어떻게 될까요?
# 해당 코드는 어떤 문제점을 가지고 있을까요? 그 이유도 같이 설명해주세요.

class A:
  def __init__(self, name, parent=None):
    self.name = name
    self.parent = parent
    self.children = set()

  def __del__(self):
    print("delete", self.name)


def main():
  a = A(name=1)
  a.children.add(A(name=2, parent=a))
  print("end")

if __name__ == "__main__":
  main()

 

answer

end
delete 1
delete 2

 

출력은 앞서 설명한 GC 방식을 이용하여 end부터 먼저 출력되고, 그 뒤로 (delete, self.name)이 출력된다.

그렇다면 문제가 뭔지 계속 고민해보았는데, 여기서 좀 막혀서 많이 여쭤보면서 공부했다.

 

문제점

  1. 순환 참조: 부모 객체와 자식 객체 사이에 순환 참조(set(a))가 있다. a 객체는 a.children 세트 내에 자식 객체를 포함하고 있으며, 자식 객체의 parent 속성은 다시 a를 참조하고 있다. 이는 순환 참조로 인해 메모리 누수를 발생시킬 수 있다. 이러한 순환 참조를 방지하기 위해 약한 참조(weak reference)를 제시받았다.
  2. __del__ 메서드에서 출력: __del__ 메서드 내에서 출력을 사용하는 것은 좋은 습관이 아니라고 다. 객체가 삭제될 때 출력을 하면 예상치 못한 결과가 발생할 수 있다. 대신에 디버깅을 위해 출력을 사용할 경우, __del__ 메서드 대신 다른 방법을 사용하는 것이 좋다.

아래 코드는 어떤 문제점을 가지고 있을까요? 메모리 누수가 일어난다고 하면 얼마나 일어날까요?

그 이유도 같이 설명해주세요.

class A:
  def __init__(self, name, parent=None):
    self.name = name
    self.parent = parent
    self.children = set()
    self.workload = ' ' * 128 * 1024 * 1024

  def __del__(self):
    print("delete", self.name)

def main():
  for _ in range(10):
    a = A(name=1)
    a.children.add(A(name=2, parent=a))

  print("end")


if __name__ == "__main__":
  main()

 

문제점

먼저 메모리 누수 측면에서 설명하자면, A 클래스의 __init__ 메서드에서 self.workload를 초기화할 때 매우 큰 문자열을 할당하고 있다. 이로 인해 각 A 객체가 생성될 때마다 매우 많은 메모리가 할당되며, 이 메모리는 사용되지 않고 유지되기 때문에 메모리 누수를 초래할 수 있다.

또한 이름 충돌이 발생할 수 있다. main 함수에서 A 클래스의 객체를 생성할 때 name 파라미터를 1로 고정하고 있지만, 이는 각 객체가 동일한 이름을 가지게 되어 객체들 간에 구별이 어렵게 된다. 이는 프로그램의 가독성과 디버깅을 어렵게 할 수 있다.


번외로 또 A객체 self.children에서 set()을 하여 순환참조가 이루어지고 있는 점도 문제점이 될 수 있다.

 


Django Queryset에 대한 이해도

  • Django에서 아래 코드를 실행 시키면 실제 evaluated 되는 시점은 언제인가요?
queryset = User.objects.filter(id=1234)

new_queryset = queryset

for q in queryset:
  print(q)

for q in queryset:
  print(q)

 

첫 번째 루프에서 for q in queryset: 문이 실행될 때, 데이터베이스로의 쿼리가 실행되고 데이터가 가져와진다. 이 때 Queryset이 평가되어 데이터베이스에서 데이터가 가져와진다.

두 번째 루프에서도 마찬가지로 for q in queryset: 문이 실행된다. 그러나 이 시점에서는 이미 Queryset이 평가되었기 때문에 이전에 가져온 데이터가 캐시되어 있으므로 데이터베이스로의 추가 쿼리가 실행되지 않고 캐시된 데이터가 사용된다.

 

  • 아래 코드의 차이점은 무엇일까요? 왜 성능차이가 발생하는 걸까요?
queryset.count() vs len(queryset)

 

 

1. queryset.count()
SQL 쿼리를 실행하여 데이터베이스에서 실제로 행의 수를 반환한다. 따라서 이 방법은 데이터베이스에서 집계를 수행하여 결과를 가져온다. 이는 데이터베이스에 쿼리를 보내고 결과를 가져오는 데 오버헤드가 발생할 수 있다. 그러나 이 메서드는 항상 정확한 결과를 제공한다.

 

2. len(queryset)

Django ORM이 데이터베이스로 쿼리를 실행하지 않고, 이미 메모리에 로드된 queryset을 사용하여 Python의 내장 함수인 len()을 호출한다. 이는 데이터베이스에 쿼리를 보내는 대신, 이미 가져온 결과의 길이를 반환하기 때문에 데이터베이스에 대한 추가적인 쿼리를 수행하지 않는다. 그러나 이 방법은 이미 queryset이 메모리에 로드되어 있어야 하며, 때로는 메모리 사용량이 많아질 수 있다.

 

일반적으로 DB와 상호작용이 적은 len(queryset)이 더 빠르나, 메모리 사용에 주의해야 한다. 성능 차이는 이러한 DB와의 상호작용 정도의 차이에 따라 발생하게 되기 때문이다.

 


알고리즘 프로그래밍

사람은 한번에 한 칸 혹은 두 칸의 계단을 오를 수 있다. 계단을 오르는데 필요한 비용(cost)이 있다고 가정할 때, 꼭대기까지 오를 수 있는 최소비용은 얼마인가?

 

사실 문제를 이해하는 데 시간이 조금 걸렸다. 코딩 사이트들 처럼 parameter 예시 값들이 없으니 조금 이해하는 것에 시간이 걸렸는데, 더 집중해서 볼 필요가 있었음을 느꼈다.

문제를 이해하고, DP 문제임을 금방 깨달았다. 생각보다 알고리즘 풀이 난이도는 높지 않았는데, 문제를 이해하는 게 더 중요했다고 느꼈다.

def min_cost_to_top(cost):
    n = len(cost)
    if n == 0:
        return 0
    elif n == 1:
        return cost[0]

    # dp 배열 초기화
    dp = [0] * (n + 1)
    dp[0] = cost[0]
    dp[1] = cost[1]

    for i in range(2, n):
        dp[i] = cost[i] + min(dp[i - 1], dp[i - 2])

    return min(dp[n - 1], dp[n - 2])

'Computer Science > 기타 CS' 카테고리의 다른 글

에자일 방법론이란?  (1) 2023.10.14
프로그래밍에서 SOLID란?  (0) 2023.09.26